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 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
- ```bash
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
- ```javascript
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
- ```bash
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
- ```javascript
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
- ```javascript
116
+ ```typescript
113
117
  import type {
114
118
  DocumentState,
115
119
  VisualRow,
@@ -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
- export interface CliEditor extends TEditingMethods, TClipboardMethods, TNavigationMethods, TRenderingMethods, TSearchMethods, THistoryMethods, TIOMethods, TKeyHandlingMethods, TSelectionMethods {
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;
@@ -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 {};
@@ -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
  };
@@ -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);
@@ -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.insertSoftTab();
281
- return true;
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();
@@ -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
- * Định nghĩa giao diện cho một sự kiện keypress.
3
- * Được điều chỉnh từ file editor.ts.
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
- * Hàm chính, chấp nhận một Readable Stream làm cho nó
15
- * phát ra sự kiện "keypress".
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;
@@ -1,11 +1,11 @@
1
1
  // src/vendor/keypress.ts
2
- // Đây phiên bản "vendored" của thư viện 'keypress' (0.2.1)
3
- // được chuyển đổi sang TypeScript loại bỏ hỗ trợ chuột
4
- // để tích hợp trực tiếp vào cliedit.
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
- * Hàm polyfill cho `EventEmitter.listenerCount()`, để tương thích ngược.
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 dùng để phân tích escape code của ansi
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
- * Hàm chính, chấp nhận một Readable Stream làm cho nó
24
- * phát ra sự kiện "keypress".
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
- // Gắn decoder vào stream để theo dõi
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
- // Không ai đang nghe, gỡ bỏ listener
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
- * Kiểm tra xem stream đã phát ra sự kiện "keypress" hay chưa.
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: Đối với các phiên bản node cũ, chúng ta muốn xóa các
63
- // listener "data" "newListener" hiện chúng sẽ không
64
- // bao gồm các phần mở rộng của module này (như "mousepress" đã bị loại bỏ).
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: Ép kiểu 'l' thành kiểu listener hợp lệ
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: Ép kiểu 'l' thành kiểu listener hợp lệ
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
- * Phần code bên dưới được lấy từ module `readline.js` của node-core
82
- * đã được chuyển đổi sang TypeScript.
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
- // Cảnh báo: Block `Buffer.isBuffer(s)` đã bị loại bỏ.
95
- // Lý do: `onData` luôn gọi `emitKey` với một string (kết quả từ StringDecoder).
96
- // Block đệ quy (paste) cũng gọi với string.
97
- // Vì vậy, `s` luôn string.
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, đáng lẽ phải linefeed
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 hoặc ctrl+h
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
- // Lắp ráp lại key code, bỏ qua \x1b đứng đầu,
160
- // bitflag của phím bổ trợ và bất kỳ chuỗi "1;" vô nghĩa nào
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: Chuyển đổi (parts[...]) sang number bằng parseInt
165
+ // FIX TS2362: Convert (parts[...]) to number using parseInt
166
166
  const modifier = parseInt(parts[3] || parts[5] || '1', 10) - 1;
167
- // Phân tích phím bổ trợ
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
- // Phân tích chính phím đó
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
- // Nhận được một chuỗi tự dài hơn một.
419
- // thể paste, không phải control sequence.
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
- // Không phát ra key nếu không tìm thấy tên
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.3.0",
4
- "description": "A lightweight, raw-mode terminal editor utility for Node.js CLI applications, with line wrapping and undo/redo support.",
5
- "repository": "https://github.com/CodeTease/cliedit",
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",