cliedit 0.3.0 → 0.4.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/README.md +9 -5
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +8 -0
- package/dist/editor.d.ts +4 -1
- package/dist/editor.editing.d.ts +21 -0
- package/dist/editor.editing.js +133 -0
- package/dist/editor.history.js +2 -0
- package/dist/editor.js +3 -0
- package/dist/editor.keys.js +29 -3
- package/dist/editor.rendering.js +9 -0
- package/dist/editor.syntax.d.ts +16 -0
- package/dist/editor.syntax.js +84 -0
- package/dist/vendor/keypress.d.ts +4 -4
- package/dist/vendor/keypress.js +31 -31
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -14,16 +14,20 @@ It includes line wrapping, visual navigation, smart auto-indentation, undo/redo,
|
|
|
14
14
|
- **Undo/Redo:** `Ctrl+Z` / `Ctrl+Y` for persistent history.
|
|
15
15
|
- **Text Selection:** `Ctrl+Arrow` keys to select text.
|
|
16
16
|
- **Clipboard Support:** `Ctrl+C` (Copy), `Ctrl+X` (Cut), `Ctrl+V` (Paste) for system clipboard (macOS, Windows, **and Linux** via `xclip`).
|
|
17
|
+
- **Syntax Highlighting:** Lightweight highlighting for Brackets `()` `[]` `{}` and Strings `""` `''`.
|
|
17
18
|
- **File I/O:** Loads from and saves to the filesystem.
|
|
18
19
|
- **Search & Replace:** `Ctrl+W` to find text, `Ctrl+R` to find and replace interactively.
|
|
19
20
|
- **Go to Line:** `Ctrl+L` to quickly jump to a specific line number.
|
|
20
21
|
- **Smart Auto-Indentation:** Automatically preserves indentation level when pressing Enter.
|
|
22
|
+
- **Block Indentation:** Use `Tab` / `Shift+Tab` to indent or outdent selected blocks of text.
|
|
21
23
|
- **Smart Navigation:** `Alt + Left/Right` to jump by words, `Ctrl + M` to jump between matching brackets.
|
|
24
|
+
- **Line Moving:** `Alt + Up/Down` to move the current line or selection up and down.
|
|
25
|
+
- **Line Duplication:** `Ctrl+D` to duplicate the current line or selection.
|
|
22
26
|
- **Piping Support:** Works with standard Unix pipes (e.g. `cat file.txt | cliedit`).
|
|
23
27
|
- **Crash Recovery:** Automatically saves changes to a hidden swap file (e.g. `.filename.swp`) to prevent data loss.
|
|
24
28
|
|
|
25
29
|
## Installation
|
|
26
|
-
```
|
|
30
|
+
```shell
|
|
27
31
|
npm install cliedit
|
|
28
32
|
````
|
|
29
33
|
|
|
@@ -31,7 +35,7 @@ npm install cliedit
|
|
|
31
35
|
|
|
32
36
|
The package exports an `async` function `openEditor` that returns a `Promise`. The promise resolves when the user quits the editor.
|
|
33
37
|
|
|
34
|
-
```
|
|
38
|
+
```typescript
|
|
35
39
|
import { openEditor } from 'cliedit';
|
|
36
40
|
import path from 'path';
|
|
37
41
|
|
|
@@ -64,7 +68,7 @@ getCommitMessage();
|
|
|
64
68
|
|
|
65
69
|
`cliedit` supports standard input piping. When used in a pipeline, it reads the input content, then re-opens the TTY to allow interactive editing.
|
|
66
70
|
|
|
67
|
-
```
|
|
71
|
+
```shell
|
|
68
72
|
# Edit a file using cat
|
|
69
73
|
cat README.md | node my-app.js
|
|
70
74
|
|
|
@@ -101,7 +105,7 @@ If the process crashes or is terminated abruptly, the next time you open the fil
|
|
|
101
105
|
|
|
102
106
|
The main editor class. You can import this directly if you need to extend or instantiate the editor with custom logic.
|
|
103
107
|
|
|
104
|
-
```
|
|
108
|
+
```typescript
|
|
105
109
|
import { CliEditor } from 'cliedit';
|
|
106
110
|
```
|
|
107
111
|
|
|
@@ -109,7 +113,7 @@ import { CliEditor } from 'cliedit';
|
|
|
109
113
|
|
|
110
114
|
Key types are also exported for convenience:
|
|
111
115
|
|
|
112
|
-
```
|
|
116
|
+
```typescript
|
|
113
117
|
import type {
|
|
114
118
|
DocumentState,
|
|
115
119
|
VisualRow,
|
package/dist/constants.d.ts
CHANGED
|
@@ -11,6 +11,9 @@ export declare const ANSI: {
|
|
|
11
11
|
RESET_COLORS: string;
|
|
12
12
|
ENTER_ALTERNATE_SCREEN: string;
|
|
13
13
|
EXIT_ALTERNATE_SCREEN: string;
|
|
14
|
+
YELLOW: string;
|
|
15
|
+
CYAN: string;
|
|
16
|
+
GREEN: string;
|
|
14
17
|
};
|
|
15
18
|
/**
|
|
16
19
|
* Key definitions for special keypresses (using Ctrl+ keys for reliable detection).
|
|
@@ -47,4 +50,8 @@ export declare const KEYS: {
|
|
|
47
50
|
PAGE_UP: string;
|
|
48
51
|
PAGE_DOWN: string;
|
|
49
52
|
TAB: string;
|
|
53
|
+
SHIFT_TAB: string;
|
|
54
|
+
ALT_UP: string;
|
|
55
|
+
ALT_DOWN: string;
|
|
56
|
+
CTRL_D: string;
|
|
50
57
|
};
|
package/dist/constants.js
CHANGED
|
@@ -12,6 +12,10 @@ export const ANSI = {
|
|
|
12
12
|
RESET_COLORS: '\x1b[0m', // Reset colors
|
|
13
13
|
ENTER_ALTERNATE_SCREEN: '\x1b[?1049h', // Enter alternate screen
|
|
14
14
|
EXIT_ALTERNATE_SCREEN: '\x1b[?1049l', // Exit alternate screen
|
|
15
|
+
// Syntax Highlighting Colors
|
|
16
|
+
YELLOW: '\x1b[33m',
|
|
17
|
+
CYAN: '\x1b[36m',
|
|
18
|
+
GREEN: '\x1b[32m',
|
|
15
19
|
};
|
|
16
20
|
/**
|
|
17
21
|
* Key definitions for special keypresses (using Ctrl+ keys for reliable detection).
|
|
@@ -52,4 +56,8 @@ export const KEYS = {
|
|
|
52
56
|
PAGE_UP: 'pageup',
|
|
53
57
|
PAGE_DOWN: 'pagedown',
|
|
54
58
|
TAB: '\t',
|
|
59
|
+
SHIFT_TAB: 'SHIFT_TAB', // Internal representation
|
|
60
|
+
ALT_UP: 'ALT_UP', // Internal representation
|
|
61
|
+
ALT_DOWN: 'ALT_DOWN', // Internal representation
|
|
62
|
+
CTRL_D: '\x04',
|
|
55
63
|
};
|
package/dist/editor.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { historyMethods } from './editor.history.js';
|
|
|
10
10
|
import { ioMethods } from './editor.io.js';
|
|
11
11
|
import { TKeyHandlingMethods } from './editor.keys.js';
|
|
12
12
|
import { TSelectionMethods } from './editor.selection.js';
|
|
13
|
+
import { syntaxMethods } from './editor.syntax.js';
|
|
13
14
|
type TEditingMethods = typeof editingMethods;
|
|
14
15
|
type TClipboardMethods = typeof clipboardMethods;
|
|
15
16
|
type TNavigationMethods = typeof navigationMethods;
|
|
@@ -17,7 +18,8 @@ type TRenderingMethods = typeof renderingMethods;
|
|
|
17
18
|
type TSearchMethods = typeof searchMethods;
|
|
18
19
|
type THistoryMethods = typeof historyMethods;
|
|
19
20
|
type TIOMethods = typeof ioMethods;
|
|
20
|
-
|
|
21
|
+
type TSyntaxMethods = typeof syntaxMethods;
|
|
22
|
+
export interface CliEditor extends TEditingMethods, TClipboardMethods, TNavigationMethods, TRenderingMethods, TSearchMethods, THistoryMethods, TIOMethods, TKeyHandlingMethods, TSelectionMethods, TSyntaxMethods {
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* Main editor class managing application state, TTY interaction, and rendering.
|
|
@@ -57,6 +59,7 @@ export declare class CliEditor {
|
|
|
57
59
|
end: number;
|
|
58
60
|
}>>;
|
|
59
61
|
searchResultIndex: number;
|
|
62
|
+
syntaxCache: Map<number, Map<number, string>>;
|
|
60
63
|
history: HistoryManager;
|
|
61
64
|
swapManager: SwapManager;
|
|
62
65
|
isCleanedUp: boolean;
|
package/dist/editor.editing.d.ts
CHANGED
|
@@ -37,6 +37,23 @@ declare function deleteForward(this: CliEditor): void;
|
|
|
37
37
|
* @param closeChar The corresponding closing character (e.g., ')', ']', '}').
|
|
38
38
|
*/
|
|
39
39
|
declare function handleAutoPair(this: CliEditor, openChar: string, closeChar: string): void;
|
|
40
|
+
/**
|
|
41
|
+
* Indents the selected lines (Block Indentation).
|
|
42
|
+
*/
|
|
43
|
+
declare function indentSelection(this: CliEditor): void;
|
|
44
|
+
/**
|
|
45
|
+
* Outdents the selected lines (Block Outdent).
|
|
46
|
+
*/
|
|
47
|
+
declare function outdentSelection(this: CliEditor): void;
|
|
48
|
+
/**
|
|
49
|
+
* Moves the current line or selection up or down.
|
|
50
|
+
* @param direction -1 for Up, 1 for Down
|
|
51
|
+
*/
|
|
52
|
+
declare function moveLines(this: CliEditor, direction: -1 | 1): void;
|
|
53
|
+
/**
|
|
54
|
+
* Duplicates the current line or selection.
|
|
55
|
+
*/
|
|
56
|
+
declare function duplicateLineOrSelection(this: CliEditor): void;
|
|
40
57
|
export declare const editingMethods: {
|
|
41
58
|
insertContentAtCursor: typeof insertContentAtCursor;
|
|
42
59
|
insertCharacter: typeof insertCharacter;
|
|
@@ -45,5 +62,9 @@ export declare const editingMethods: {
|
|
|
45
62
|
deleteBackward: typeof deleteBackward;
|
|
46
63
|
deleteForward: typeof deleteForward;
|
|
47
64
|
handleAutoPair: typeof handleAutoPair;
|
|
65
|
+
indentSelection: typeof indentSelection;
|
|
66
|
+
outdentSelection: typeof outdentSelection;
|
|
67
|
+
moveLines: typeof moveLines;
|
|
68
|
+
duplicateLineOrSelection: typeof duplicateLineOrSelection;
|
|
48
69
|
};
|
|
49
70
|
export {};
|
package/dist/editor.editing.js
CHANGED
|
@@ -33,6 +33,7 @@ function insertContentAtCursor(contentLines) {
|
|
|
33
33
|
this.cursorX = lastPasteLine.length;
|
|
34
34
|
}
|
|
35
35
|
this.setDirty();
|
|
36
|
+
this.invalidateSyntaxCache();
|
|
36
37
|
this.recalculateVisualRows();
|
|
37
38
|
}
|
|
38
39
|
/**
|
|
@@ -42,6 +43,7 @@ function insertCharacter(char) {
|
|
|
42
43
|
const line = this.lines[this.cursorY] || '';
|
|
43
44
|
this.lines[this.cursorY] = line.slice(0, this.cursorX) + char + line.slice(this.cursorX);
|
|
44
45
|
this.cursorX += char.length;
|
|
46
|
+
this.invalidateSyntaxCache();
|
|
45
47
|
}
|
|
46
48
|
/**
|
|
47
49
|
* Inserts a soft tab (using configured tabSize).
|
|
@@ -49,6 +51,7 @@ function insertCharacter(char) {
|
|
|
49
51
|
function insertSoftTab() {
|
|
50
52
|
const spaces = ' '.repeat(this.tabSize || 4);
|
|
51
53
|
this.insertCharacter(spaces);
|
|
54
|
+
// invalidation handled in insertCharacter
|
|
52
55
|
}
|
|
53
56
|
/**
|
|
54
57
|
* Inserts a new line, splitting the current line at the cursor position.
|
|
@@ -66,6 +69,7 @@ function insertNewLine() {
|
|
|
66
69
|
this.cursorY++;
|
|
67
70
|
this.cursorX = indent.length; // Move cursor to end of indent
|
|
68
71
|
this.setDirty();
|
|
72
|
+
this.invalidateSyntaxCache();
|
|
69
73
|
}
|
|
70
74
|
/**
|
|
71
75
|
* Deletes the character before the cursor, or joins the current line with the previous one.
|
|
@@ -90,6 +94,7 @@ function deleteBackward() {
|
|
|
90
94
|
this.cursorX = 0;
|
|
91
95
|
}
|
|
92
96
|
this.setDirty();
|
|
97
|
+
this.invalidateSyntaxCache();
|
|
93
98
|
}
|
|
94
99
|
/**
|
|
95
100
|
* Deletes the character after the cursor, or joins the current line with the next one.
|
|
@@ -110,6 +115,7 @@ function deleteForward() {
|
|
|
110
115
|
this.cursorX = 0;
|
|
111
116
|
}
|
|
112
117
|
this.setDirty();
|
|
118
|
+
this.invalidateSyntaxCache();
|
|
113
119
|
}
|
|
114
120
|
/**
|
|
115
121
|
* Handles auto-pairing of brackets and quotes.
|
|
@@ -141,6 +147,129 @@ function handleAutoPair(openChar, closeChar) {
|
|
|
141
147
|
}
|
|
142
148
|
this.setDirty();
|
|
143
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Indents the selected lines (Block Indentation).
|
|
152
|
+
*/
|
|
153
|
+
function indentSelection() {
|
|
154
|
+
this.saveState(); // Save state before modification for Undo
|
|
155
|
+
const selection = this.getNormalizedSelection();
|
|
156
|
+
if (!selection)
|
|
157
|
+
return;
|
|
158
|
+
for (let i = selection.start.y; i <= selection.end.y; i++) {
|
|
159
|
+
const line = this.lines[i];
|
|
160
|
+
this.lines[i] = ' '.repeat(this.tabSize) + line;
|
|
161
|
+
}
|
|
162
|
+
// Adjust selection anchors
|
|
163
|
+
if (this.selectionAnchor) {
|
|
164
|
+
this.selectionAnchor.x += this.tabSize;
|
|
165
|
+
this.cursorX += this.tabSize;
|
|
166
|
+
}
|
|
167
|
+
this.setDirty();
|
|
168
|
+
this.invalidateSyntaxCache();
|
|
169
|
+
this.recalculateVisualRows();
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Outdents the selected lines (Block Outdent).
|
|
173
|
+
*/
|
|
174
|
+
function outdentSelection() {
|
|
175
|
+
this.saveState(); // Save state before modification for Undo
|
|
176
|
+
// If no selection, try to outdent current line
|
|
177
|
+
let startY = this.cursorY;
|
|
178
|
+
let endY = this.cursorY;
|
|
179
|
+
if (this.selectionAnchor) {
|
|
180
|
+
const selection = this.getNormalizedSelection();
|
|
181
|
+
if (selection) {
|
|
182
|
+
startY = selection.start.y;
|
|
183
|
+
endY = selection.end.y;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
let changed = false;
|
|
187
|
+
for (let i = startY; i <= endY; i++) {
|
|
188
|
+
const line = this.lines[i];
|
|
189
|
+
// Remove up to tabSize spaces
|
|
190
|
+
const match = line.match(/^(\s+)/);
|
|
191
|
+
if (match) {
|
|
192
|
+
const spaces = match[1].length;
|
|
193
|
+
const toRemove = Math.min(spaces, this.tabSize);
|
|
194
|
+
this.lines[i] = line.slice(toRemove);
|
|
195
|
+
changed = true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (changed) {
|
|
199
|
+
if (this.selectionAnchor) {
|
|
200
|
+
// Approximation: shift anchor and cursor left
|
|
201
|
+
this.selectionAnchor.x = Math.max(0, this.selectionAnchor.x - this.tabSize);
|
|
202
|
+
this.cursorX = Math.max(0, this.cursorX - this.tabSize);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
this.cursorX = Math.max(0, this.cursorX - this.tabSize);
|
|
206
|
+
}
|
|
207
|
+
this.setDirty();
|
|
208
|
+
this.invalidateSyntaxCache();
|
|
209
|
+
this.recalculateVisualRows();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Moves the current line or selection up or down.
|
|
214
|
+
* @param direction -1 for Up, 1 for Down
|
|
215
|
+
*/
|
|
216
|
+
function moveLines(direction) {
|
|
217
|
+
this.saveState(); // Save state before modification for Undo
|
|
218
|
+
let startY = this.cursorY;
|
|
219
|
+
let endY = this.cursorY;
|
|
220
|
+
if (this.selectionAnchor) {
|
|
221
|
+
const selection = this.getNormalizedSelection();
|
|
222
|
+
if (selection) {
|
|
223
|
+
startY = selection.start.y;
|
|
224
|
+
endY = selection.end.y;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Boundary checks
|
|
228
|
+
if (direction === -1 && startY === 0)
|
|
229
|
+
return; // Top
|
|
230
|
+
if (direction === 1 && endY >= this.lines.length - 1)
|
|
231
|
+
return; // Bottom
|
|
232
|
+
// Extract lines to move
|
|
233
|
+
const count = endY - startY + 1;
|
|
234
|
+
const linesToMove = this.lines.splice(startY, count);
|
|
235
|
+
// Insert at new position
|
|
236
|
+
const newStart = startY + direction;
|
|
237
|
+
this.lines.splice(newStart, 0, ...linesToMove);
|
|
238
|
+
// Update selection/cursor
|
|
239
|
+
this.cursorY += direction;
|
|
240
|
+
if (this.selectionAnchor) {
|
|
241
|
+
this.selectionAnchor.y += direction;
|
|
242
|
+
}
|
|
243
|
+
this.setDirty();
|
|
244
|
+
this.recalculateVisualRows();
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Duplicates the current line or selection.
|
|
248
|
+
*/
|
|
249
|
+
function duplicateLineOrSelection() {
|
|
250
|
+
this.saveState(); // Save state before modification for Undo
|
|
251
|
+
if (this.selectionAnchor) {
|
|
252
|
+
const selection = this.getNormalizedSelection();
|
|
253
|
+
if (!selection)
|
|
254
|
+
return;
|
|
255
|
+
const text = this.getSelectedText();
|
|
256
|
+
// We need to move cursor to end of selection.
|
|
257
|
+
// Normalized selection end:
|
|
258
|
+
this.cursorX = selection.end.x;
|
|
259
|
+
this.cursorY = selection.end.y;
|
|
260
|
+
const contentLines = text.split('\n');
|
|
261
|
+
this.insertContentAtCursor(contentLines);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
// Single line duplication
|
|
265
|
+
const line = this.lines[this.cursorY];
|
|
266
|
+
this.lines.splice(this.cursorY + 1, 0, line);
|
|
267
|
+
this.cursorY++; // Move down to the new line
|
|
268
|
+
// CursorX stays same? Usually yes.
|
|
269
|
+
}
|
|
270
|
+
this.setDirty();
|
|
271
|
+
this.recalculateVisualRows();
|
|
272
|
+
}
|
|
144
273
|
export const editingMethods = {
|
|
145
274
|
insertContentAtCursor,
|
|
146
275
|
insertCharacter,
|
|
@@ -149,4 +278,8 @@ export const editingMethods = {
|
|
|
149
278
|
deleteBackward,
|
|
150
279
|
deleteForward,
|
|
151
280
|
handleAutoPair,
|
|
281
|
+
indentSelection,
|
|
282
|
+
outdentSelection,
|
|
283
|
+
moveLines,
|
|
284
|
+
duplicateLineOrSelection,
|
|
152
285
|
};
|
package/dist/editor.history.js
CHANGED
|
@@ -38,6 +38,7 @@ function undo() {
|
|
|
38
38
|
if (state) {
|
|
39
39
|
this.loadState(state);
|
|
40
40
|
this.setDirty();
|
|
41
|
+
this.invalidateSyntaxCache();
|
|
41
42
|
this.setStatusMessage('Undo successful');
|
|
42
43
|
}
|
|
43
44
|
else {
|
|
@@ -52,6 +53,7 @@ function redo() {
|
|
|
52
53
|
if (state) {
|
|
53
54
|
this.loadState(state);
|
|
54
55
|
this.setDirty();
|
|
56
|
+
this.invalidateSyntaxCache();
|
|
55
57
|
this.setStatusMessage('Redo successful');
|
|
56
58
|
}
|
|
57
59
|
else {
|
package/dist/editor.js
CHANGED
|
@@ -14,6 +14,7 @@ import { historyMethods } from './editor.history.js';
|
|
|
14
14
|
import { ioMethods } from './editor.io.js';
|
|
15
15
|
import { keyHandlingMethods } from './editor.keys.js';
|
|
16
16
|
import { selectionMethods } from './editor.selection.js';
|
|
17
|
+
import { syntaxMethods } from './editor.syntax.js';
|
|
17
18
|
const DEFAULT_STATUS = 'HELP: Ctrl+S = Save | Ctrl+Q = Quit | Ctrl+W = Find | Ctrl+R = Replace | Ctrl+L = Go to Line';
|
|
18
19
|
/**
|
|
19
20
|
* Main editor class managing application state, TTY interaction, and rendering.
|
|
@@ -44,6 +45,7 @@ export class CliEditor {
|
|
|
44
45
|
// Map<lineNumber, Array<{ start, end }>> for fast rendering lookup
|
|
45
46
|
this.searchResultMap = new Map();
|
|
46
47
|
this.searchResultIndex = -1;
|
|
48
|
+
this.syntaxCache = new Map();
|
|
47
49
|
this.isCleanedUp = false;
|
|
48
50
|
this.resolvePromise = null;
|
|
49
51
|
this.rejectPromise = null;
|
|
@@ -145,3 +147,4 @@ Object.assign(CliEditor.prototype, historyMethods);
|
|
|
145
147
|
Object.assign(CliEditor.prototype, ioMethods);
|
|
146
148
|
Object.assign(CliEditor.prototype, keyHandlingMethods);
|
|
147
149
|
Object.assign(CliEditor.prototype, selectionMethods);
|
|
150
|
+
Object.assign(CliEditor.prototype, syntaxMethods);
|
package/dist/editor.keys.js
CHANGED
|
@@ -80,11 +80,15 @@ function handleKeypressEvent(ch, key) {
|
|
|
80
80
|
else if (key.name === 'return')
|
|
81
81
|
keyName = KEYS.ENTER;
|
|
82
82
|
else if (key.name === 'tab')
|
|
83
|
-
keyName = KEYS.TAB;
|
|
83
|
+
keyName = key.shift ? KEYS.SHIFT_TAB : KEYS.TAB;
|
|
84
84
|
else if (key.meta && key.name === 'left')
|
|
85
85
|
keyName = 'ALT_LEFT';
|
|
86
86
|
else if (key.meta && key.name === 'right')
|
|
87
87
|
keyName = 'ALT_RIGHT';
|
|
88
|
+
else if (key.meta && key.name === 'up')
|
|
89
|
+
keyName = KEYS.ALT_UP;
|
|
90
|
+
else if (key.meta && key.name === 'down')
|
|
91
|
+
keyName = KEYS.ALT_DOWN;
|
|
88
92
|
// Handle Mouse Scroll events explicitly
|
|
89
93
|
else if (key.name === 'scrollup')
|
|
90
94
|
keyName = 'SCROLL_UP';
|
|
@@ -277,8 +281,30 @@ function handleEditKeys(key) {
|
|
|
277
281
|
return true;
|
|
278
282
|
case KEYS.TAB:
|
|
279
283
|
this.clearSearchResults();
|
|
280
|
-
this.
|
|
281
|
-
|
|
284
|
+
if (this.selectionAnchor) {
|
|
285
|
+
this.indentSelection();
|
|
286
|
+
return false; // Manually saved state
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
this.insertSoftTab();
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
case KEYS.SHIFT_TAB:
|
|
293
|
+
this.clearSearchResults();
|
|
294
|
+
this.outdentSelection();
|
|
295
|
+
return false; // Manually saved state
|
|
296
|
+
case KEYS.ALT_UP:
|
|
297
|
+
this.clearSearchResults();
|
|
298
|
+
this.moveLines(-1);
|
|
299
|
+
return false; // Manually saved state
|
|
300
|
+
case KEYS.ALT_DOWN:
|
|
301
|
+
this.clearSearchResults();
|
|
302
|
+
this.moveLines(1);
|
|
303
|
+
return false; // Manually saved state
|
|
304
|
+
case KEYS.CTRL_D:
|
|
305
|
+
this.clearSearchResults();
|
|
306
|
+
this.duplicateLineOrSelection();
|
|
307
|
+
return false; // Manually saved state
|
|
282
308
|
// --- Search & History ---
|
|
283
309
|
case KEYS.CTRL_W:
|
|
284
310
|
this.enterFindMode();
|
package/dist/editor.rendering.js
CHANGED
|
@@ -63,6 +63,9 @@ function render() {
|
|
|
63
63
|
: ' '.padStart(this.gutterWidth - 2, ' ') + ' | ';
|
|
64
64
|
buffer += lineNumber;
|
|
65
65
|
let lineContent = row.content;
|
|
66
|
+
// Retrieve syntax color map for the full logical line
|
|
67
|
+
// We pass the full line content because the scanner needs context
|
|
68
|
+
const syntaxColorMap = this.getLineSyntaxColor(row.logicalY, this.lines[row.logicalY]);
|
|
66
69
|
// 2. Draw Content (Character by Character for selection/cursor)
|
|
67
70
|
for (let i = 0; i < lineContent.length; i++) {
|
|
68
71
|
const char = lineContent[i];
|
|
@@ -87,6 +90,8 @@ function render() {
|
|
|
87
90
|
this.searchResults[this.searchResultIndex]?.y === logicalY &&
|
|
88
91
|
logicalX >= this.searchResults[this.searchResultIndex]?.x &&
|
|
89
92
|
logicalX < (this.searchResults[this.searchResultIndex]?.x + this.searchQuery.length));
|
|
93
|
+
// Syntax highlight color
|
|
94
|
+
const syntaxColor = syntaxColorMap.get(logicalX);
|
|
90
95
|
if (isSelected) {
|
|
91
96
|
buffer += ANSI.INVERT_COLORS + char + ANSI.RESET_COLORS;
|
|
92
97
|
}
|
|
@@ -102,6 +107,10 @@ function render() {
|
|
|
102
107
|
// Global Match: Invert only
|
|
103
108
|
buffer += ANSI.INVERT_COLORS + char + ANSI.RESET_COLORS;
|
|
104
109
|
}
|
|
110
|
+
else if (syntaxColor) {
|
|
111
|
+
// Apply syntax color
|
|
112
|
+
buffer += syntaxColor + char + ANSI.RESET_COLORS;
|
|
113
|
+
}
|
|
105
114
|
else {
|
|
106
115
|
buffer += char;
|
|
107
116
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CliEditor } from './editor.js';
|
|
2
|
+
/**
|
|
3
|
+
* Single-pass character scanner to generate a color map for a line.
|
|
4
|
+
* Implements "Poor Man's Syntax Highlighting" focusing on Brackets and Quotes.
|
|
5
|
+
*/
|
|
6
|
+
declare function getLineSyntaxColor(this: CliEditor, lineIndex: number, lineContent: string): Map<number, string>;
|
|
7
|
+
/**
|
|
8
|
+
* Invalidates the syntax highlighting cache.
|
|
9
|
+
* Clears the entire cache to be safe and simple ("Poor Man's" approach).
|
|
10
|
+
*/
|
|
11
|
+
declare function invalidateSyntaxCache(this: CliEditor): void;
|
|
12
|
+
export declare const syntaxMethods: {
|
|
13
|
+
getLineSyntaxColor: typeof getLineSyntaxColor;
|
|
14
|
+
invalidateSyntaxCache: typeof invalidateSyntaxCache;
|
|
15
|
+
};
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ANSI } from './constants.js';
|
|
2
|
+
// Syntax colors
|
|
3
|
+
const COLOR_BRACKET_1 = ANSI.YELLOW;
|
|
4
|
+
const COLOR_STRING = ANSI.GREEN;
|
|
5
|
+
// State constants
|
|
6
|
+
const STATE_NORMAL = 0;
|
|
7
|
+
const STATE_IN_STRING_SINGLE = 1; // '
|
|
8
|
+
const STATE_IN_STRING_DOUBLE = 2; // "
|
|
9
|
+
/**
|
|
10
|
+
* Single-pass character scanner to generate a color map for a line.
|
|
11
|
+
* Implements "Poor Man's Syntax Highlighting" focusing on Brackets and Quotes.
|
|
12
|
+
*/
|
|
13
|
+
function getLineSyntaxColor(lineIndex, lineContent) {
|
|
14
|
+
// Check cache first
|
|
15
|
+
if (this.syntaxCache.has(lineIndex)) {
|
|
16
|
+
return this.syntaxCache.get(lineIndex);
|
|
17
|
+
}
|
|
18
|
+
const colorMap = new Map();
|
|
19
|
+
let state = STATE_NORMAL;
|
|
20
|
+
for (let i = 0; i < lineContent.length; i++) {
|
|
21
|
+
const char = lineContent[i];
|
|
22
|
+
if (state === STATE_NORMAL) {
|
|
23
|
+
if (char === '"') {
|
|
24
|
+
state = STATE_IN_STRING_DOUBLE;
|
|
25
|
+
colorMap.set(i, COLOR_STRING);
|
|
26
|
+
}
|
|
27
|
+
else if (char === "'") {
|
|
28
|
+
state = STATE_IN_STRING_SINGLE;
|
|
29
|
+
colorMap.set(i, COLOR_STRING);
|
|
30
|
+
}
|
|
31
|
+
else if ('()[]{}'.includes(char)) {
|
|
32
|
+
// Alternate bracket colors for fun, or just use one
|
|
33
|
+
colorMap.set(i, COLOR_BRACKET_1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (state === STATE_IN_STRING_DOUBLE) {
|
|
37
|
+
colorMap.set(i, COLOR_STRING);
|
|
38
|
+
if (char === '"') {
|
|
39
|
+
// Check if escaped
|
|
40
|
+
let backslashCount = 0;
|
|
41
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
42
|
+
if (lineContent[j] === '\\')
|
|
43
|
+
backslashCount++;
|
|
44
|
+
else
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
// Even backslashes => not escaped (e.g., \\" is literal backslash then quote)
|
|
48
|
+
// Odd backslashes => escaped (e.g., \" is literal quote)
|
|
49
|
+
if (backslashCount % 2 === 0) {
|
|
50
|
+
state = STATE_NORMAL;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else if (state === STATE_IN_STRING_SINGLE) {
|
|
55
|
+
colorMap.set(i, COLOR_STRING);
|
|
56
|
+
if (char === "'") {
|
|
57
|
+
// Check if escaped
|
|
58
|
+
let backslashCount = 0;
|
|
59
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
60
|
+
if (lineContent[j] === '\\')
|
|
61
|
+
backslashCount++;
|
|
62
|
+
else
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
if (backslashCount % 2 === 0) {
|
|
66
|
+
state = STATE_NORMAL;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
this.syntaxCache.set(lineIndex, colorMap);
|
|
72
|
+
return colorMap;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Invalidates the syntax highlighting cache.
|
|
76
|
+
* Clears the entire cache to be safe and simple ("Poor Man's" approach).
|
|
77
|
+
*/
|
|
78
|
+
function invalidateSyntaxCache() {
|
|
79
|
+
this.syntaxCache.clear();
|
|
80
|
+
}
|
|
81
|
+
export const syntaxMethods = {
|
|
82
|
+
getLineSyntaxColor,
|
|
83
|
+
invalidateSyntaxCache
|
|
84
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Defines the interface for a keypress event.
|
|
3
|
+
* Adapted from the editor.ts file.
|
|
4
4
|
*/
|
|
5
5
|
export interface KeypressEvent {
|
|
6
6
|
name?: string;
|
|
@@ -11,7 +11,7 @@ export interface KeypressEvent {
|
|
|
11
11
|
code?: string;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Main function, accepts a Readable Stream and makes it
|
|
15
|
+
* emit "keypress" events.
|
|
16
16
|
*/
|
|
17
17
|
export default function keypress(stream: NodeJS.ReadStream): void;
|
package/dist/vendor/keypress.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// src/vendor/keypress.ts
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// This is a "vendored" version of the 'keypress' library (0.2.1)
|
|
3
|
+
// converted to TypeScript and stripped of mouse support
|
|
4
|
+
// to be integrated directly into cliedit.
|
|
5
5
|
import { EventEmitter } from 'events';
|
|
6
6
|
import { StringDecoder } from 'string_decoder';
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Polyfill for `EventEmitter.listenerCount()`, for backward compatibility.
|
|
9
9
|
*/
|
|
10
10
|
let listenerCount = EventEmitter.listenerCount;
|
|
11
11
|
if (!listenerCount) {
|
|
@@ -14,19 +14,19 @@ if (!listenerCount) {
|
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Regexes
|
|
17
|
+
* Regexes used to parse ansi escape codes.
|
|
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
21
|
const mouseSgrRe = /^\x1b\[<(\d+);(\d+);(\d+)([mM])/;
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* Main function, accepts a Readable Stream and makes it
|
|
24
|
+
* emit "keypress" events.
|
|
25
25
|
*/
|
|
26
26
|
export default function keypress(stream) {
|
|
27
27
|
if (isEmittingKeypress(stream))
|
|
28
28
|
return;
|
|
29
|
-
//
|
|
29
|
+
// Attach decoder to the stream to monitor data
|
|
30
30
|
stream._keypressDecoder = new StringDecoder('utf8');
|
|
31
31
|
function onData(b) {
|
|
32
32
|
if (listenerCount(stream, 'keypress') > 0) {
|
|
@@ -35,7 +35,7 @@ export default function keypress(stream) {
|
|
|
35
35
|
emitKey(stream, r);
|
|
36
36
|
}
|
|
37
37
|
else {
|
|
38
|
-
//
|
|
38
|
+
// No one is listening, remove listener
|
|
39
39
|
stream.removeListener('data', onData);
|
|
40
40
|
stream.on('newListener', onNewListener);
|
|
41
41
|
}
|
|
@@ -54,23 +54,23 @@ export default function keypress(stream) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
|
-
*
|
|
57
|
+
* Checks if the stream has already emitted the "keypress" event.
|
|
58
58
|
*/
|
|
59
59
|
function isEmittingKeypress(stream) {
|
|
60
60
|
let rtn = !!stream._keypressDecoder;
|
|
61
61
|
if (!rtn) {
|
|
62
|
-
// XXX:
|
|
63
|
-
//
|
|
64
|
-
//
|
|
62
|
+
// XXX: For older node versions, we want to remove existing
|
|
63
|
+
// "data" and "newListener" listeners because they won't
|
|
64
|
+
// include extensions from this module (like "mousepress" which was removed).
|
|
65
65
|
stream.listeners('data').slice(0).forEach(function (l) {
|
|
66
66
|
if (l.name === 'onData' && /emitKey/.test(l.toString())) {
|
|
67
|
-
// FIX TS2769:
|
|
67
|
+
// FIX TS2769: Cast 'l' to a valid listener type
|
|
68
68
|
stream.removeListener('data', l);
|
|
69
69
|
}
|
|
70
70
|
});
|
|
71
71
|
stream.listeners('newListener').slice(0).forEach(function (l) {
|
|
72
72
|
if (l.name === 'onNewListener' && /keypress/.test(l.toString())) {
|
|
73
|
-
// FIX TS2769:
|
|
73
|
+
// FIX TS2769: Cast 'l' to a valid listener type
|
|
74
74
|
stream.removeListener('newListener', l);
|
|
75
75
|
}
|
|
76
76
|
});
|
|
@@ -78,8 +78,8 @@ function isEmittingKeypress(stream) {
|
|
|
78
78
|
return rtn;
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
81
|
+
* The code below is taken from node-core's `readline.js` module
|
|
82
|
+
* and has been converted to TypeScript.
|
|
83
83
|
*/
|
|
84
84
|
function emitKey(stream, s) {
|
|
85
85
|
let ch;
|
|
@@ -91,16 +91,16 @@ function emitKey(stream, s) {
|
|
|
91
91
|
sequence: s,
|
|
92
92
|
};
|
|
93
93
|
let parts;
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
94
|
+
// Warning: The `Buffer.isBuffer(s)` block has been removed.
|
|
95
|
+
// Reason: `onData` always calls `emitKey` with a string (result from StringDecoder).
|
|
96
|
+
// The recursive block (paste) also calls with a string.
|
|
97
|
+
// Therefore, `s` is always a string.
|
|
98
98
|
if (s === '\r') {
|
|
99
99
|
// carriage return
|
|
100
100
|
key.name = 'return';
|
|
101
101
|
}
|
|
102
102
|
else if (s === '\n') {
|
|
103
|
-
// enter,
|
|
103
|
+
// enter, should have been linefeed
|
|
104
104
|
key.name = 'enter';
|
|
105
105
|
}
|
|
106
106
|
else if (s === '\t') {
|
|
@@ -111,7 +111,7 @@ function emitKey(stream, s) {
|
|
|
111
111
|
s === '\x7f' ||
|
|
112
112
|
s === '\x1b\x7f' ||
|
|
113
113
|
s === '\x1b\b') {
|
|
114
|
-
// backspace
|
|
114
|
+
// backspace or ctrl+h
|
|
115
115
|
key.name = 'backspace';
|
|
116
116
|
key.meta = s.charAt(0) === '\x1b';
|
|
117
117
|
}
|
|
@@ -156,20 +156,20 @@ function emitKey(stream, s) {
|
|
|
156
156
|
}
|
|
157
157
|
else if ((parts = functionKeyCodeRe.exec(s))) {
|
|
158
158
|
// ansi escape sequence
|
|
159
|
-
//
|
|
160
|
-
// bitflag
|
|
159
|
+
// Reassemble key code, ignoring leading \x1b,
|
|
160
|
+
// modifier bitflag, and any meaningless "1;" strings
|
|
161
161
|
const code = (parts[1] || '') +
|
|
162
162
|
(parts[2] || '') +
|
|
163
163
|
(parts[4] || '') +
|
|
164
164
|
(parts[6] || '');
|
|
165
|
-
// FIX TS2362:
|
|
165
|
+
// FIX TS2362: Convert (parts[...]) to number using parseInt
|
|
166
166
|
const modifier = parseInt(parts[3] || parts[5] || '1', 10) - 1;
|
|
167
|
-
//
|
|
167
|
+
// Parse modifier keys
|
|
168
168
|
key.ctrl = !!(modifier & 4);
|
|
169
169
|
key.meta = !!(modifier & 10);
|
|
170
170
|
key.shift = !!(modifier & 1);
|
|
171
171
|
key.code = code;
|
|
172
|
-
//
|
|
172
|
+
// Parse the key itself
|
|
173
173
|
switch (code) {
|
|
174
174
|
/* xterm/gnome ESC O letter */
|
|
175
175
|
case 'OP':
|
|
@@ -415,8 +415,8 @@ function emitKey(stream, s) {
|
|
|
415
415
|
}
|
|
416
416
|
}
|
|
417
417
|
else if (s.length > 1 && s[0] !== '\x1b') {
|
|
418
|
-
//
|
|
419
|
-
//
|
|
418
|
+
// Received a string longer than one character.
|
|
419
|
+
// Could be a paste, since it's not a control sequence.
|
|
420
420
|
for (const c of s) {
|
|
421
421
|
emitKey(stream, c);
|
|
422
422
|
}
|
|
@@ -448,7 +448,7 @@ function emitKey(stream, s) {
|
|
|
448
448
|
// We can handle click here if needed (b=0 left, b=1 middle, b=2 right)
|
|
449
449
|
// but for now only scroll is requested.
|
|
450
450
|
}
|
|
451
|
-
//
|
|
451
|
+
// Don't emit key if name is not found
|
|
452
452
|
if (key.name === undefined) {
|
|
453
453
|
return; // key = undefined;
|
|
454
454
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cliedit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A
|
|
5
|
-
"repository":
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "A zero-dependency, embeddable TUI text editor for Node.js CLI applications.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/CodeTease/cliedit.git"
|
|
8
|
+
},
|
|
6
9
|
"type": "module",
|
|
7
10
|
"main": "dist/index.js",
|
|
8
11
|
"types": "dist/index.d.ts",
|