cliedit 0.4.0 → 0.5.1

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
@@ -6,6 +6,8 @@ A lightweight, zero-dependency, raw-mode terminal editor component for Node.js.
6
6
 
7
7
  It includes line wrapping, visual navigation, smart auto-indentation, undo/redo, text selection, Find/Replace, and cross-platform clipboard support.
8
8
 
9
+ A **CodeTease** project.
10
+
9
11
  ## Features
10
12
 
11
13
  - **Raw Mode TTY:** Takes over the terminal for a full "app-like" feel.
@@ -26,10 +28,19 @@ It includes line wrapping, visual navigation, smart auto-indentation, undo/redo,
26
28
  - **Piping Support:** Works with standard Unix pipes (e.g. `cat file.txt | cliedit`).
27
29
  - **Crash Recovery:** Automatically saves changes to a hidden swap file (e.g. `.filename.swp`) to prevent data loss.
28
30
 
31
+ ### Architecture Improvements
32
+
33
+ `cliedit` employs a optimization strategy to handle large files efficiently while maintaining a responsive UI:
34
+
35
+ * **Math-Only Viewport:** Rendering is stateless. The editor calculates visual wrapping on the fly (Virtual Scrolling) rather than storing a massive state array, significantly reducing memory usage for large documents.
36
+ * **Screen Buffer Diffing:** A double-buffering system compares the current and next frame to send only the changed characters to the terminal, minimizing I/O and eliminating flicker.
37
+ * **Worker Threads:** Syntax highlighting runs asynchronously in a background Worker Thread, preventing UI freezes during rendering of complex lines.
38
+ * **Recommended Limits:** Good for files up to 50k lines (perfect for configs, scripts, and logs).
39
+
29
40
  ## Installation
30
41
  ```shell
31
42
  npm install cliedit
32
- ````
43
+ ```
33
44
 
34
45
  ## Usage
35
46
 
@@ -116,7 +127,6 @@ Key types are also exported for convenience:
116
127
  ```typescript
117
128
  import type {
118
129
  DocumentState,
119
- VisualRow,
120
130
  EditorMode,
121
131
  NormalizedRange,
122
132
  } from 'cliedit';
@@ -9,6 +9,7 @@ export declare const ANSI: {
9
9
  SHOW_CURSOR: string;
10
10
  INVERT_COLORS: string;
11
11
  RESET_COLORS: string;
12
+ DIM: string;
12
13
  ENTER_ALTERNATE_SCREEN: string;
13
14
  EXIT_ALTERNATE_SCREEN: string;
14
15
  YELLOW: string;
package/dist/constants.js CHANGED
@@ -10,6 +10,7 @@ export const ANSI = {
10
10
  SHOW_CURSOR: '\x1b[?25h', // Show cursor
11
11
  INVERT_COLORS: '\x1b[7m', // Invert background/foreground colors
12
12
  RESET_COLORS: '\x1b[0m', // Reset colors
13
+ DIM: '\x1b[2m', // Dim mode (faint)
13
14
  ENTER_ALTERNATE_SCREEN: '\x1b[?1049h', // Enter alternate screen
14
15
  EXIT_ALTERNATE_SCREEN: '\x1b[?1049l', // Exit alternate screen
15
16
  // Syntax Highlighting Colors
@@ -15,7 +15,7 @@ function setClipboard(text) {
15
15
  case 'win32':
16
16
  command = 'clip';
17
17
  break;
18
- case 'linux': // <--- THÊM HỖ TRỢ LINUX
18
+ case 'linux':
19
19
  command = 'xclip -selection clipboard';
20
20
  break;
21
21
  default:
@@ -46,8 +46,8 @@ function getClipboard() {
46
46
  case 'win32':
47
47
  command = 'powershell -command "Get-Clipboard"';
48
48
  break;
49
- case 'linux': // <--- THÊM HỖ TRỢ LINUX
50
- command = 'xclip -selection clipboard -o'; // -o (hoặc -out) để đọc
49
+ case 'linux':
50
+ command = 'xclip -selection clipboard -o';
51
51
  break;
52
52
  default:
53
53
  this.setStatusMessage('Clipboard not supported on this platform');
@@ -99,6 +99,7 @@ async function pasteLine() {
99
99
  this.insertContentAtCursor(pasteLines);
100
100
  }
101
101
  catch (error) {
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
102
103
  this.setStatusMessage(`Paste failed: ${error.message}`);
103
104
  }
104
105
  }
package/dist/editor.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { HistoryManager } from './history.js';
2
- import { VisualRow, EditorMode, EditorOptions } from './types.js';
2
+ import { EditorMode, EditorOptions } from './types.js';
3
3
  import { SwapManager } from './editor.swap.js';
4
+ import { ScreenBuffer } from './screen_buffer.js';
5
+ import { Worker } from 'worker_threads';
4
6
  import { editingMethods } from './editor.editing.js';
5
7
  import { clipboardMethods } from './editor.clipboard.js';
6
8
  import { navigationMethods } from './editor.navigation.js';
@@ -40,7 +42,6 @@ export declare class CliEditor {
40
42
  gutterWidth: number;
41
43
  tabSize: number;
42
44
  screenStartRow: number;
43
- visualRows: VisualRow[];
44
45
  mode: EditorMode;
45
46
  statusMessage: string;
46
47
  statusTimeout: NodeJS.Timeout | null;
@@ -60,17 +61,20 @@ export declare class CliEditor {
60
61
  }>>;
61
62
  searchResultIndex: number;
62
63
  syntaxCache: Map<number, Map<number, string>>;
64
+ syntaxWorker: Worker | null;
63
65
  history: HistoryManager;
64
66
  swapManager: SwapManager;
67
+ screenBuffer: ScreenBuffer;
65
68
  isCleanedUp: boolean;
66
69
  resolvePromise: ((value: {
67
70
  saved: boolean;
68
71
  content: string;
69
72
  }) => void) | null;
70
- rejectPromise: ((reason?: any) => void) | null;
71
- inputStream: any;
73
+ rejectPromise: ((reason?: unknown) => void) | null;
74
+ inputStream: NodeJS.ReadStream;
72
75
  isExiting: boolean;
73
76
  constructor(initialContent: string, filepath: string, options?: EditorOptions);
77
+ private handleWorkerMessage;
74
78
  run(): Promise<{
75
79
  saved: boolean;
76
80
  content: string;
@@ -34,7 +34,6 @@ function insertContentAtCursor(contentLines) {
34
34
  }
35
35
  this.setDirty();
36
36
  this.invalidateSyntaxCache();
37
- this.recalculateVisualRows();
38
37
  }
39
38
  /**
40
39
  * Inserts a single character at the cursor position.
@@ -166,7 +165,6 @@ function indentSelection() {
166
165
  }
167
166
  this.setDirty();
168
167
  this.invalidateSyntaxCache();
169
- this.recalculateVisualRows();
170
168
  }
171
169
  /**
172
170
  * Outdents the selected lines (Block Outdent).
@@ -206,7 +204,6 @@ function outdentSelection() {
206
204
  }
207
205
  this.setDirty();
208
206
  this.invalidateSyntaxCache();
209
- this.recalculateVisualRows();
210
207
  }
211
208
  }
212
209
  /**
@@ -241,7 +238,6 @@ function moveLines(direction) {
241
238
  this.selectionAnchor.y += direction;
242
239
  }
243
240
  this.setDirty();
244
- this.recalculateVisualRows();
245
241
  }
246
242
  /**
247
243
  * Duplicates the current line or selection.
@@ -268,7 +264,6 @@ function duplicateLineOrSelection() {
268
264
  // CursorX stays same? Usually yes.
269
265
  }
270
266
  this.setDirty();
271
- this.recalculateVisualRows();
272
267
  }
273
268
  export const editingMethods = {
274
269
  insertContentAtCursor,
@@ -10,7 +10,7 @@ declare function getCurrentState(this: CliEditor): DocumentState;
10
10
  /**
11
11
  * Saves the current state to the history manager.
12
12
  */
13
- declare function saveState(this: CliEditor, initial?: boolean): void;
13
+ declare function saveState(this: CliEditor, _initial?: boolean): void;
14
14
  /**
15
15
  * Loads a document state from the history manager.
16
16
  */
@@ -16,7 +16,7 @@ function getCurrentState() {
16
16
  /**
17
17
  * Saves the current state to the history manager.
18
18
  */
19
- function saveState(initial = false) {
19
+ function saveState(_initial = false) {
20
20
  // Only save if content is different from the last state,
21
21
  // but ALWAYS save the initial state.
22
22
  this.history.saveState(this.getCurrentState());
package/dist/editor.io.js CHANGED
@@ -22,6 +22,7 @@ async function saveFile() {
22
22
  this.setStatusMessage(`Saved: ${this.filepath}`, 2000);
23
23
  }
24
24
  catch (err) {
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
26
  this.setStatusMessage(`Save Error: ${err.message}`);
26
27
  }
27
28
  }
package/dist/editor.js CHANGED
@@ -1,9 +1,13 @@
1
- // Cập nhật 1: Import từ ./vendor/keypress.js
1
+ // src/editor.ts
2
+ /* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
2
3
  import keypress from './vendor/keypress.js';
3
4
  import { ANSI } from './constants.js';
4
5
  import { HistoryManager } from './history.js';
5
6
  import { SwapManager } from './editor.swap.js';
6
- // Block `declare module 'keypress'` đã bị xóa
7
+ import { ScreenBuffer } from './screen_buffer.js';
8
+ import { Worker } from 'worker_threads';
9
+ import { fileURLToPath } from 'url';
10
+ import { dirname, join } from 'path';
7
11
  // Import all functional modules
8
12
  import { editingMethods } from './editor.editing.js';
9
13
  import { clipboardMethods } from './editor.clipboard.js';
@@ -31,7 +35,6 @@ export class CliEditor {
31
35
  this.gutterWidth = 5;
32
36
  this.tabSize = 4;
33
37
  this.screenStartRow = 1;
34
- this.visualRows = [];
35
38
  this.mode = 'edit';
36
39
  this.statusMessage = DEFAULT_STATUS;
37
40
  this.statusTimeout = null;
@@ -46,6 +49,7 @@ export class CliEditor {
46
49
  this.searchResultMap = new Map();
47
50
  this.searchResultIndex = -1;
48
51
  this.syntaxCache = new Map();
52
+ this.syntaxWorker = null;
49
53
  this.isCleanedUp = false;
50
54
  this.resolvePromise = null;
51
55
  this.rejectPromise = null;
@@ -60,9 +64,35 @@ export class CliEditor {
60
64
  this.tabSize = options.tabSize ?? 4;
61
65
  this.inputStream = options.inputStream || process.stdin;
62
66
  this.history = new HistoryManager();
67
+ this.screenBuffer = new ScreenBuffer();
63
68
  this.saveState(true);
64
69
  // Initialize SwapManager
65
70
  this.swapManager = new SwapManager(this.filepath, () => this.lines.join('\n'));
71
+ // Initialize Worker
72
+ try {
73
+ const __filename = fileURLToPath(import.meta.url);
74
+ const __dirname = dirname(__filename);
75
+ // Assuming compiled code is in dist/ and syntax.worker.js is there.
76
+ const workerPath = join(__dirname, 'syntax.worker.js');
77
+ this.syntaxWorker = new Worker(workerPath);
78
+ this.syntaxWorker.on('message', this.handleWorkerMessage.bind(this));
79
+ }
80
+ catch {
81
+ // Fallback or log error
82
+ // console.error("Failed to load worker", e);
83
+ }
84
+ }
85
+ handleWorkerMessage(msg) {
86
+ const { lineIndex, colorMap } = msg;
87
+ this.syntaxCache.set(lineIndex, colorMap);
88
+ // Trigger Partial Render?
89
+ // For simplicity, just render. The screen buffer diffing handles optimization.
90
+ // But we only need to render IF the line is currently visible?
91
+ // Checking visibility is optimization.
92
+ // Let's just render.
93
+ if (!this.isCleanedUp) {
94
+ this.render();
95
+ }
66
96
  }
67
97
  // --- Lifecycle Methods ---
68
98
  run() {
@@ -72,6 +102,7 @@ export class CliEditor {
72
102
  return new Promise((resolve, reject) => {
73
103
  const performCleanup = (callback) => {
74
104
  this.swapManager.stop(); // Stop swap interval
105
+ this.syntaxWorker?.terminate();
75
106
  if (this.isCleanedUp) {
76
107
  if (callback)
77
108
  callback();
@@ -113,7 +144,11 @@ export class CliEditor {
113
144
  throw new Error('Editor requires a TTY environment (stdout).');
114
145
  }
115
146
  this.updateScreenSize();
116
- this.recalculateVisualRows();
147
+ this.screenBuffer.resize(this.screenRows + 2, this.screenCols); // +2 for Status Bar space if needed?
148
+ // screenRows = stdout.rows - 2.
149
+ // ScreenBuffer should cover the FULL terminal size (rows, cols) to handle status bar rendering too.
150
+ // So pass process.stdout.rows.
151
+ this.screenBuffer.resize(process.stdout.rows, process.stdout.columns);
117
152
  // Enter alternate screen and hide cursor + Enable SGR Mouse (1006) and Button Event (1000)
118
153
  process.stdout.write(ANSI.ENTER_ALTERNATE_SCREEN + ANSI.HIDE_CURSOR + ANSI.CLEAR_SCREEN + '\x1b[?1000h' + '\x1b[?1006h');
119
154
  if (this.inputStream.setRawMode) {
@@ -128,7 +163,7 @@ export class CliEditor {
128
163
  }
129
164
  handleResize() {
130
165
  this.updateScreenSize();
131
- this.recalculateVisualRows();
166
+ this.screenBuffer.resize(process.stdout.rows, process.stdout.columns);
132
167
  this.render();
133
168
  }
134
169
  updateScreenSize() {
@@ -11,13 +11,12 @@ const PAIR_MAP = {
11
11
  * Main router for standardized keypress events from the 'keypress' library.
12
12
  */
13
13
  function handleKeypressEvent(ch, key) {
14
- // CRASH FIX: If the editor is already closing, ignore all input.
15
14
  if (this.isExiting) {
16
15
  return;
17
16
  }
18
17
  let keyName = undefined;
19
18
  let edited = false;
20
- // --- 1. Xử trường hợp key null/undefined ( tự in được) ---
19
+ // --- 1. Handle the case where key is null/undefined (Printable characters) ---
21
20
  if (!key) {
22
21
  if (ch && ch.length === 1 && ch >= ' ' && ch <= '~') {
23
22
  if (this.mode === 'search_find' || this.mode === 'search_replace') {
@@ -30,7 +29,6 @@ function handleKeypressEvent(ch, key) {
30
29
  edited = this.handleEditKeys(ch);
31
30
  if (edited) {
32
31
  this.saveState();
33
- this.recalculateVisualRows(); // Phải tính toán lại sau khi gõ
34
32
  }
35
33
  }
36
34
  else if (this.mode === 'search_confirm') {
@@ -41,8 +39,8 @@ function handleKeypressEvent(ch, key) {
41
39
  }
42
40
  return;
43
41
  }
44
- // --- 2. Từ đây, 'key' object đảm bảo (phím đặc biệt hoặc Ctrl/Meta) ---
45
- // 2.1. Ánh xạ Control sequences (Ctrl+Arrow cho selection)
42
+ // --- 2. From here, the 'key' object is guaranteed to exist (special keys or Ctrl/Meta) ---
43
+ // 2.1. Map Control sequences (Ctrl+Arrow for selection)
46
44
  if (key.ctrl) {
47
45
  if (key.name === 'up')
48
46
  keyName = KEYS.CTRL_ARROW_UP;
@@ -56,8 +54,17 @@ function handleKeypressEvent(ch, key) {
56
54
  keyName = key.sequence;
57
55
  }
58
56
  else {
59
- // 2.2. Ánh xạ phím tiêu chuẩn (Arrow, Home, End, Enter, Tab)
60
- if (key.name === 'up')
57
+ // 2.2. Map standard keys (Arrow, Home, End, Enter, Tab)
58
+ // Check Meta keys first!
59
+ if (key.meta && key.name === 'left')
60
+ keyName = 'ALT_LEFT';
61
+ else if (key.meta && key.name === 'right')
62
+ keyName = 'ALT_RIGHT';
63
+ else if (key.meta && key.name === 'up')
64
+ keyName = KEYS.ALT_UP;
65
+ else if (key.meta && key.name === 'down')
66
+ keyName = KEYS.ALT_DOWN;
67
+ else if (key.name === 'up')
61
68
  keyName = KEYS.ARROW_UP;
62
69
  else if (key.name === 'down')
63
70
  keyName = KEYS.ARROW_DOWN;
@@ -81,14 +88,6 @@ function handleKeypressEvent(ch, key) {
81
88
  keyName = KEYS.ENTER;
82
89
  else if (key.name === 'tab')
83
90
  keyName = key.shift ? KEYS.SHIFT_TAB : KEYS.TAB;
84
- else if (key.meta && key.name === 'left')
85
- keyName = 'ALT_LEFT';
86
- else if (key.meta && key.name === 'right')
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;
92
91
  // Handle Mouse Scroll events explicitly
93
92
  else if (key.name === 'scrollup')
94
93
  keyName = 'SCROLL_UP';
@@ -97,7 +96,7 @@ function handleKeypressEvent(ch, key) {
97
96
  else
98
97
  keyName = key.sequence;
99
98
  }
100
- // --- 3. Định tuyến theo Mode ---
99
+ // --- 3. Route according to Mode ---
101
100
  if (this.mode === 'search_find' || this.mode === 'search_replace') {
102
101
  this.handleSearchKeys(keyName || ch);
103
102
  }
@@ -108,7 +107,7 @@ function handleKeypressEvent(ch, key) {
108
107
  this.handleGoToLineKeys(keyName || ch);
109
108
  }
110
109
  else {
111
- // 4. Xử phím lựa chọn (Ctrl+Arrow) - Navigation
110
+ // 4. Handle selection keys (Ctrl+Arrow) - Navigation
112
111
  switch (keyName) {
113
112
  case KEYS.CTRL_ARROW_UP:
114
113
  case KEYS.CTRL_ARROW_DOWN:
@@ -126,22 +125,27 @@ function handleKeypressEvent(ch, key) {
126
125
  this.render();
127
126
  return;
128
127
  }
129
- // 5. Xử tất cả các phím lệnh/chỉnh sửa khác
128
+ // 5. Handle all other command/edit keys
130
129
  if (keyName === 'SCROLL_UP') {
131
130
  const scrollAmount = 3;
132
131
  this.rowOffset = Math.max(0, this.rowOffset - scrollAmount);
133
132
  // Adjust cursor if it falls out of view (below the viewport)
134
133
  // Actually if we scroll UP, the viewport moves UP. The cursor might be BELOW the viewport.
135
- // Wait, scroll UP means viewing lines ABOVE. Viewport index decreases.
134
+ // scroll UP means viewing lines ABOVE. Viewport index decreases.
136
135
  // Cursor (if previously in view) might now be >= rowOffset + screenRows.
137
136
  // We need to ensure cursor is within [rowOffset, rowOffset + screenRows - 1]
138
137
  // But verify after setting rowOffset.
139
138
  const currentVisualRow = this.findCurrentVisualRowIndex();
140
139
  const bottomEdge = this.rowOffset + this.screenRows - 1;
141
140
  if (currentVisualRow > bottomEdge) {
142
- const targetRow = this.visualRows[bottomEdge];
143
- this.cursorY = targetRow.logicalY;
144
- this.cursorX = targetRow.logicalXStart;
141
+ // Move cursor to bottom edge
142
+ // Need logic to move cursor to visual row 'bottomEdge'
143
+ // Use getLogicalFromVisual
144
+ const targetPos = this.getLogicalFromVisual(bottomEdge);
145
+ this.cursorY = targetPos.logicalY;
146
+ // Set to start of that chunk
147
+ const contentWidth = Math.max(1, this.screenCols - this.gutterWidth);
148
+ this.cursorX = targetPos.visualYOffset * contentWidth;
145
149
  }
146
150
  else if (currentVisualRow < this.rowOffset) {
147
151
  // Should not happen when scrolling up (moving viewport up), unless cursor was already above?
@@ -155,35 +159,44 @@ function handleKeypressEvent(ch, key) {
155
159
  // if (currentVisualRow >= this.rowOffset + this.screenRows) -> this.rowOffset = ...
156
160
  // So we MUST move cursor inside the new viewport.
157
161
  if (currentVisualRow > bottomEdge) {
158
- const targetRow = this.visualRows[bottomEdge];
159
- this.cursorY = targetRow.logicalY;
160
- this.cursorX = targetRow.logicalXStart;
162
+ const targetPos = this.getLogicalFromVisual(bottomEdge);
163
+ this.cursorY = targetPos.logicalY;
164
+ const contentWidth = Math.max(1, this.screenCols - this.gutterWidth);
165
+ this.cursorX = targetPos.visualYOffset * contentWidth;
161
166
  }
162
167
  }
163
168
  else if (keyName === 'SCROLL_DOWN') {
164
169
  const scrollAmount = 3;
165
- const maxOffset = Math.max(0, this.visualRows.length - this.screenRows);
170
+ // Need total visual rows to clamp maxOffset.
171
+ // Calculating total visual rows is O(N).
172
+ // Let's do a safe scroll?
173
+ // Or just allow scrolling until end?
174
+ // Let's calculate total visual rows for now (performance cost accepted for correct scrolling).
175
+ let totalVisualRows = 0;
176
+ for (let i = 0; i < this.lines.length; i++)
177
+ totalVisualRows += this.getLineVisualHeight(i);
178
+ const maxOffset = Math.max(0, totalVisualRows - this.screenRows);
166
179
  this.rowOffset = Math.min(maxOffset, this.rowOffset + scrollAmount);
167
180
  // Scroll DOWN means viewport index increases.
168
181
  // Cursor might be ABOVE the new viewport (currentVisualRow < rowOffset).
169
182
  const currentVisualRow = this.findCurrentVisualRowIndex();
170
183
  if (currentVisualRow < this.rowOffset) {
171
- const targetRow = this.visualRows[this.rowOffset];
172
- this.cursorY = targetRow.logicalY;
173
- this.cursorX = targetRow.logicalXStart;
184
+ const targetPos = this.getLogicalFromVisual(this.rowOffset);
185
+ this.cursorY = targetPos.logicalY;
186
+ const contentWidth = Math.max(1, this.screenCols - this.gutterWidth);
187
+ this.cursorX = targetPos.visualYOffset * contentWidth;
174
188
  }
175
189
  }
176
190
  else {
177
191
  edited = this.handleEditKeys(keyName || ch);
178
192
  }
179
193
  }
180
- // 6. Cập nhật Trạng thái và Render
194
+ // 6. Update State and Render
181
195
  if (edited) {
182
- this.saveState(); // <-- Chỉ gọi khi gõ phím, xóa, v.v.
183
- this.recalculateVisualRows(); // Tính toán lại layout
196
+ this.saveState(); // <-- Called only when typing, deleting, etc.
184
197
  }
185
198
  if (!this.isExiting) {
186
- this.render(); // Render cuối cùng (với visual rows đã được cập nhật nếu cần)
199
+ this.render(); // Final render (with visual rows updated if necessary)
187
200
  }
188
201
  }
189
202
  function handleAltArrows(keyName) {
@@ -251,7 +264,7 @@ function handleEditKeys(key) {
251
264
  this.clearSearchResults();
252
265
  this.insertNewLine();
253
266
  return true;
254
- case KEYS.BACKSPACE:
267
+ case KEYS.BACKSPACE: {
255
268
  this.clearSearchResults();
256
269
  // Handle auto-pair deletion
257
270
  const line = this.lines[this.cursorY] || '';
@@ -272,6 +285,7 @@ function handleEditKeys(key) {
272
285
  this.deleteBackward();
273
286
  }
274
287
  return true;
288
+ }
275
289
  case KEYS.DELETE:
276
290
  this.clearSearchResults();
277
291
  if (this.selectionAnchor)
@@ -332,15 +346,12 @@ function handleEditKeys(key) {
332
346
  // For now, let's use Ctrl+B?
333
347
  this.matchBracket();
334
348
  return false;
335
- // ***** SỬA LỖI VISUAL *****
336
- // Sau khi undo/redo, chúng ta PHẢI tính toán lại visual rows
349
+ // After undo/redo, we MUST recalculate visual rows
337
350
  case KEYS.CTRL_Z:
338
351
  this.undo();
339
- this.recalculateVisualRows(); // <-- THÊM DÒNG NÀY
340
352
  return false;
341
353
  case KEYS.CTRL_Y:
342
354
  this.redo();
343
- this.recalculateVisualRows(); // <-- THÊM DÒNG NÀY
344
355
  return false;
345
356
  // --- Clipboard ---
346
357
  case KEYS.CTRL_K: // Cut Line (Traditional)
@@ -355,7 +366,7 @@ function handleEditKeys(key) {
355
366
  case KEYS.CTRL_V: // Paste Selection
356
367
  this.pasteSelection();
357
368
  return true;
358
- // Xử Ký tự in được
369
+ // Handle Printable Characters
359
370
  default:
360
371
  if (key.length === 1 && key >= ' ' && key <= '~') {
361
372
  this.clearSearchResults();
@@ -536,7 +547,7 @@ function handleGoToLineKeys(key) {
536
547
  this.setStatusMessage('Cancelled');
537
548
  };
538
549
  switch (key) {
539
- case KEYS.ENTER:
550
+ case KEYS.ENTER: {
540
551
  const lineNumber = parseInt(this.goToLineQuery, 10);
541
552
  if (!isNaN(lineNumber) && lineNumber > 0) {
542
553
  this.jumpToLine(lineNumber);
@@ -547,6 +558,7 @@ function handleGoToLineKeys(key) {
547
558
  }
548
559
  this.goToLineQuery = '';
549
560
  break;
561
+ }
550
562
  case KEYS.ESCAPE:
551
563
  case KEYS.CTRL_C:
552
564
  case KEYS.CTRL_Q:
@@ -4,6 +4,7 @@ import { CliEditor } from './editor.js';
4
4
  */
5
5
  /**
6
6
  * Finds the index of the visual row that currently contains the cursor.
7
+ * Uses math to calculate position based on line lengths and screen width.
7
8
  */
8
9
  declare function findCurrentVisualRowIndex(this: CliEditor): number;
9
10
  /**
@@ -38,6 +39,8 @@ declare function jumpToLine(this: CliEditor, lineNumber: number): void;
38
39
  * Enters Go To Line mode.
39
40
  */
40
41
  declare function enterGoToLineMode(this: CliEditor): void;
42
+ declare function moveCursorByWord(this: CliEditor, direction: 'left' | 'right'): void;
43
+ declare function matchBracket(this: CliEditor): void;
41
44
  export declare const navigationMethods: {
42
45
  findCurrentVisualRowIndex: typeof findCurrentVisualRowIndex;
43
46
  moveCursorLogically: typeof moveCursorLogically;
@@ -51,6 +54,4 @@ export declare const navigationMethods: {
51
54
  moveCursorByWord: typeof moveCursorByWord;
52
55
  matchBracket: typeof matchBracket;
53
56
  };
54
- declare function moveCursorByWord(this: CliEditor, direction: 'left' | 'right'): void;
55
- declare function matchBracket(this: CliEditor): void;
56
57
  export {};