cliedit 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,19 +8,34 @@ function handleKeypressEvent(ch, key) {
8
8
  if (this.isExiting) {
9
9
  return;
10
10
  }
11
- // CRASH FIX: Handle case where 'key' is undefined (a normal character key)
11
+ let keyName = undefined;
12
+ let edited = false;
13
+ // --- 1. Xử lý trường hợp key là null/undefined (Ký tự in được) ---
12
14
  if (!key) {
13
- if (ch && ch >= ' ' && ch <= '~') {
14
- this.handleCharacterKey(ch);
15
- this.recalculateVisualRows();
15
+ if (ch && ch.length === 1 && ch >= ' ' && ch <= '~') {
16
+ if (this.mode === 'search_find' || this.mode === 'search_replace') {
17
+ this.handleSearchKeys(ch);
18
+ }
19
+ else if (this.mode === 'goto_line') {
20
+ this.handleGoToLineKeys(ch);
21
+ }
22
+ else if (this.mode === 'edit') {
23
+ edited = this.handleEditKeys(ch);
24
+ if (edited) {
25
+ this.saveState();
26
+ this.recalculateVisualRows(); // Phải tính toán lại sau khi gõ
27
+ }
28
+ }
29
+ else if (this.mode === 'search_confirm') {
30
+ this.handleSearchConfirmKeys(ch);
31
+ }
16
32
  this.render();
33
+ return;
17
34
  }
18
35
  return;
19
36
  }
20
- // --- From here, 'key' object is guaranteed to exist ---
21
- let keyName = undefined;
22
- let edited = false;
23
- // 1. Map Control sequences (Ctrl+Arrow for selection)
37
+ // --- 2. Từ đây, 'key' object đảm bảo (phím đặc biệt hoặc Ctrl/Meta) ---
38
+ // 2.1. Ánh xạ Control sequences (Ctrl+Arrow cho selection)
24
39
  if (key.ctrl) {
25
40
  if (key.name === 'up')
26
41
  keyName = KEYS.CTRL_ARROW_UP;
@@ -31,10 +46,10 @@ function handleKeypressEvent(ch, key) {
31
46
  else if (key.name === 'right')
32
47
  keyName = KEYS.CTRL_ARROW_RIGHT;
33
48
  else
34
- keyName = key.sequence; // Use sequence for Ctrl+S, Ctrl+C, etc.
49
+ keyName = key.sequence;
35
50
  }
36
51
  else {
37
- // 2. (FIXED) Map standard navigation keys (Arrow, Home, End)
52
+ // 2.2. Ánh xạ phím tiêu chuẩn (Arrow, Home, End, Enter, Tab)
38
53
  if (key.name === 'up')
39
54
  keyName = KEYS.ARROW_UP;
40
55
  else if (key.name === 'down')
@@ -60,55 +75,47 @@ function handleKeypressEvent(ch, key) {
60
75
  else if (key.name === 'tab')
61
76
  keyName = KEYS.TAB;
62
77
  else
63
- keyName = key.sequence; // Fallback
78
+ keyName = key.sequence;
64
79
  }
65
- // 3. (FIXED) Handle printable characters immediately
66
- // This was the source of the "no typing" bug.
67
- // We must check for characters *before* routing to handleEditKeys.
68
- if (keyName && keyName.length === 1 && keyName >= ' ' && keyName <= '~' && !key.ctrl && !key.meta) {
69
- this.handleCharacterKey(keyName);
70
- this.recalculateVisualRows();
71
- this.render();
72
- return;
73
- }
74
- // 4. Mode Routing (If it's not a character, it's a command)
75
- if (this.mode === 'search') {
80
+ // --- 3. Định tuyến theo Mode ---
81
+ if (this.mode === 'search_find' || this.mode === 'search_replace') {
76
82
  this.handleSearchKeys(keyName || ch);
77
83
  }
84
+ else if (this.mode === 'search_confirm') {
85
+ this.handleSearchConfirmKeys(keyName || ch);
86
+ }
87
+ else if (this.mode === 'goto_line') {
88
+ this.handleGoToLineKeys(keyName || ch);
89
+ }
78
90
  else {
79
- // 5. Handle Selection Keys (Ctrl+Arrow)
91
+ // 4. Xử phím lựa chọn (Ctrl+Arrow) - Navigation
80
92
  switch (keyName) {
81
93
  case KEYS.CTRL_ARROW_UP:
82
- this.startOrUpdateSelection();
83
- this.moveCursorVisually(-1);
84
- this.render();
85
- return;
86
94
  case KEYS.CTRL_ARROW_DOWN:
87
- this.startOrUpdateSelection();
88
- this.moveCursorVisually(1);
89
- this.render();
90
- return;
91
95
  case KEYS.CTRL_ARROW_LEFT:
92
- this.startOrUpdateSelection();
93
- this.moveCursorLogically(-1);
94
- this.render();
95
- return;
96
96
  case KEYS.CTRL_ARROW_RIGHT:
97
97
  this.startOrUpdateSelection();
98
- this.moveCursorLogically(1);
98
+ if (keyName === KEYS.CTRL_ARROW_UP)
99
+ this.moveCursorVisually(-1);
100
+ else if (keyName === KEYS.CTRL_ARROW_DOWN)
101
+ this.moveCursorVisually(1);
102
+ else if (keyName === KEYS.CTRL_ARROW_LEFT)
103
+ this.moveCursorLogically(-1);
104
+ else if (keyName === KEYS.CTRL_ARROW_RIGHT)
105
+ this.moveCursorLogically(1);
99
106
  this.render();
100
107
  return;
101
108
  }
102
- // 6. Handle all other command keys (Editing/Commands)
109
+ // 5. Xử tất cả các phím lệnh/chỉnh sửa khác
103
110
  edited = this.handleEditKeys(keyName || ch);
104
111
  }
105
- // 7. State Update and Render
112
+ // 6. Cập nhật Trạng thái và Render
106
113
  if (edited) {
107
- this.saveState();
108
- this.recalculateVisualRows();
114
+ this.saveState(); // <-- Chỉ gọi khi gõ phím, xóa, v.v.
115
+ this.recalculateVisualRows(); // Tính toán lại layout
109
116
  }
110
117
  if (!this.isExiting) {
111
- this.render();
118
+ this.render(); // Render cuối cùng (với visual rows đã được cập nhật nếu cần)
112
119
  }
113
120
  }
114
121
  /**
@@ -116,10 +123,6 @@ function handleKeypressEvent(ch, key) {
116
123
  * Returns true if content was modified.
117
124
  */
118
125
  function handleEditKeys(key) {
119
- // (FIXED) Removed the guard clause that was blocking typing.
120
- // if (key.length === 1 && key >= ' ' && key <= '~') {
121
- // return false;
122
- // }
123
126
  // Cancel selection on normal navigation
124
127
  const isNavigation = [
125
128
  KEYS.ARROW_UP, KEYS.ARROW_DOWN, KEYS.ARROW_LEFT, KEYS.ARROW_RIGHT,
@@ -131,7 +134,6 @@ function handleEditKeys(key) {
131
134
  this.setStatusMessage(this.DEFAULT_STATUS, 0);
132
135
  }
133
136
  }
134
- // Commands that return Promises must be wrapped in a sync call here
135
137
  switch (key) {
136
138
  // --- Exit / Save ---
137
139
  case KEYS.CTRL_Q:
@@ -143,7 +145,7 @@ function handleEditKeys(key) {
143
145
  case KEYS.CTRL_C:
144
146
  this.handleCopy();
145
147
  return false;
146
- // --- Navigation ---
148
+ // --- Navigation (Non-Selection) ---
147
149
  case KEYS.ARROW_UP:
148
150
  this.moveCursorVisually(-1);
149
151
  return false;
@@ -189,17 +191,27 @@ function handleEditKeys(key) {
189
191
  return true;
190
192
  // --- Search & History ---
191
193
  case KEYS.CTRL_W:
192
- this.enterSearchMode();
194
+ this.enterFindMode();
195
+ return false;
196
+ case KEYS.CTRL_R:
197
+ this.enterReplaceMode();
198
+ return false;
199
+ case KEYS.CTRL_L:
200
+ this.enterGoToLineMode();
193
201
  return false;
194
202
  case KEYS.CTRL_G:
195
203
  this.findNext();
196
204
  return false;
205
+ // ***** SỬA LỖI VISUAL *****
206
+ // Sau khi undo/redo, chúng ta PHẢI tính toán lại visual rows
197
207
  case KEYS.CTRL_Z:
198
208
  this.undo();
199
- return true;
209
+ this.recalculateVisualRows(); // <-- THÊM DÒNG NÀY
210
+ return false;
200
211
  case KEYS.CTRL_Y:
201
212
  this.redo();
202
- return true;
213
+ this.recalculateVisualRows(); // <-- THÊM DÒNG NÀY
214
+ return false;
203
215
  // --- Clipboard ---
204
216
  case KEYS.CTRL_K: // Cut Line (Traditional)
205
217
  this.cutLine();
@@ -208,12 +220,17 @@ function handleEditKeys(key) {
208
220
  this.pasteLine();
209
221
  return true;
210
222
  case KEYS.CTRL_X: // Cut Selection
211
- this.cutSelection(); // Synchronous wrapper for cutSelectionAsync
223
+ this.cutSelection();
212
224
  return true;
213
225
  case KEYS.CTRL_V: // Paste Selection
214
226
  this.pasteSelection();
215
227
  return true;
228
+ // Xử lý Ký tự in được
216
229
  default:
230
+ if (key.length === 1 && key >= ' ' && key <= '~') {
231
+ this.handleCharacterKey(key);
232
+ return true;
233
+ }
217
234
  return false;
218
235
  }
219
236
  }
@@ -274,28 +291,130 @@ async function handleSave() {
274
291
  }
275
292
  }
276
293
  /**
277
- * Handles Search Mode input keys.
294
+ * Handles Search Mode input keys (for 'search_find' and 'search_replace').
278
295
  */
279
296
  function handleSearchKeys(key) {
297
+ const cancelSearch = () => {
298
+ this.mode = 'edit';
299
+ this.searchQuery = '';
300
+ this.replaceQuery = null;
301
+ this.searchResults = [];
302
+ this.searchResultIndex = -1;
303
+ this.setStatusMessage('Cancelled');
304
+ };
280
305
  switch (key) {
281
306
  case KEYS.ENTER:
282
- this.executeSearch();
283
- this.mode = 'edit';
307
+ if (this.mode === 'search_find') {
308
+ if (this.replaceQuery === null) {
309
+ // Find-Only Flow: Execute search and find first
310
+ this.executeSearch();
311
+ this.mode = 'edit';
312
+ this.findNext();
313
+ }
314
+ else {
315
+ // Replace Flow: Transition to get replace string
316
+ this.mode = 'search_replace';
317
+ this.setStatusMessage('Replace with: ');
318
+ }
319
+ }
320
+ else if (this.mode === 'search_replace') {
321
+ // Replace Flow: We have both strings, execute and find first
322
+ this.executeSearch();
323
+ this.mode = 'edit';
324
+ this.findNext();
325
+ }
326
+ break;
327
+ case KEYS.ESCAPE:
328
+ case KEYS.CTRL_C:
329
+ case KEYS.CTRL_Q:
330
+ cancelSearch();
331
+ break;
332
+ case KEYS.BACKSPACE:
333
+ if (this.mode === 'search_find') {
334
+ this.searchQuery = this.searchQuery.slice(0, -1);
335
+ }
336
+ else {
337
+ this.replaceQuery = this.replaceQuery.slice(0, -1);
338
+ }
339
+ break;
340
+ default:
341
+ if (key.length === 1 && key >= ' ' && key <= '~') {
342
+ if (this.mode === 'search_find') {
343
+ this.searchQuery += key;
344
+ }
345
+ else {
346
+ this.replaceQuery += key;
347
+ }
348
+ }
349
+ }
350
+ // Update status bar message live (if not cancelling)
351
+ if (this.mode === 'search_find') {
352
+ this.setStatusMessage((this.replaceQuery === null ? 'Find: ' : 'Find: ') + this.searchQuery);
353
+ }
354
+ else if (this.mode === 'search_replace') {
355
+ this.setStatusMessage('Replace with: ' + this.replaceQuery);
356
+ }
357
+ }
358
+ /**
359
+ * Handles keypresses during the (y/n/a/q) confirmation step.
360
+ */
361
+ function handleSearchConfirmKeys(key) {
362
+ switch (key.toLowerCase()) {
363
+ case 'y': // Yes
364
+ this.replaceCurrentAndFindNext();
365
+ break;
366
+ case 'n': // No
284
367
  this.findNext();
285
368
  break;
369
+ case 'a': // All
370
+ this.replaceAll();
371
+ break;
372
+ case 'q': // Quit
286
373
  case KEYS.ESCAPE:
287
374
  case KEYS.CTRL_C:
288
375
  case KEYS.CTRL_Q:
289
376
  this.mode = 'edit';
290
- this.searchQuery = '';
291
- this.setStatusMessage('Search cancelled');
377
+ this.searchResults = [];
378
+ this.searchResultIndex = -1;
379
+ this.setStatusMessage('Replace cancelled');
380
+ break;
381
+ }
382
+ }
383
+ /**
384
+ * Handles keypresses during the 'Go to Line' prompt.
385
+ */
386
+ function handleGoToLineKeys(key) {
387
+ const cancel = () => {
388
+ this.mode = 'edit';
389
+ this.goToLineQuery = '';
390
+ this.setStatusMessage('Cancelled');
391
+ };
392
+ switch (key) {
393
+ case KEYS.ENTER:
394
+ const lineNumber = parseInt(this.goToLineQuery, 10);
395
+ if (!isNaN(lineNumber) && lineNumber > 0) {
396
+ this.jumpToLine(lineNumber);
397
+ }
398
+ else {
399
+ this.mode = 'edit';
400
+ this.setStatusMessage('Invalid line number');
401
+ }
402
+ this.goToLineQuery = '';
403
+ break;
404
+ case KEYS.ESCAPE:
405
+ case KEYS.CTRL_C:
406
+ case KEYS.CTRL_Q:
407
+ cancel();
292
408
  break;
293
409
  case KEYS.BACKSPACE:
294
- this.searchQuery = this.searchQuery.slice(0, -1);
410
+ this.goToLineQuery = this.goToLineQuery.slice(0, -1);
411
+ this.setStatusMessage('Go to Line: ' + this.goToLineQuery);
295
412
  break;
296
413
  default:
297
- if (key.length === 1 && key >= ' ' && key <= '~') {
298
- this.searchQuery += key;
414
+ // Only accept digits
415
+ if (key.length === 1 && key >= '0' && key <= '9') {
416
+ this.goToLineQuery += key;
417
+ this.setStatusMessage('Go to Line: ' + this.goToLineQuery);
299
418
  }
300
419
  }
301
420
  }
@@ -303,6 +422,8 @@ export const keyHandlingMethods = {
303
422
  handleKeypressEvent,
304
423
  handleEditKeys,
305
424
  handleSearchKeys,
425
+ handleSearchConfirmKeys,
426
+ handleGoToLineKeys,
306
427
  handleCtrlQ,
307
428
  handleCopy,
308
429
  handleCharacterKey,
@@ -30,6 +30,14 @@ declare function adjustCursorPosition(this: CliEditor): void;
30
30
  * Scrolls the viewport to keep the cursor visible.
31
31
  */
32
32
  declare function scroll(this: CliEditor): void;
33
+ /**
34
+ * Jumps the cursor to a specific line number (1-based).
35
+ */
36
+ declare function jumpToLine(this: CliEditor, lineNumber: number): void;
37
+ /**
38
+ * Enters Go To Line mode.
39
+ */
40
+ declare function enterGoToLineMode(this: CliEditor): void;
33
41
  export declare const navigationMethods: {
34
42
  findCurrentVisualRowIndex: typeof findCurrentVisualRowIndex;
35
43
  moveCursorLogically: typeof moveCursorLogically;
@@ -38,5 +46,7 @@ export declare const navigationMethods: {
38
46
  findVisualRowEnd: typeof findVisualRowEnd;
39
47
  adjustCursorPosition: typeof adjustCursorPosition;
40
48
  scroll: typeof scroll;
49
+ jumpToLine: typeof jumpToLine;
50
+ enterGoToLineMode: typeof enterGoToLineMode;
41
51
  };
42
52
  export {};
@@ -121,6 +121,28 @@ function scroll() {
121
121
  this.rowOffset = currentVisualRow - this.screenRows + 1;
122
122
  }
123
123
  }
124
+ /**
125
+ * Jumps the cursor to a specific line number (1-based).
126
+ */
127
+ function jumpToLine(lineNumber) {
128
+ const targetY = lineNumber - 1; // Convert 1-based to 0-based index
129
+ // Clamp targetY to valid range
130
+ this.cursorY = Math.max(0, Math.min(targetY, this.lines.length - 1));
131
+ this.cursorX = 0; // Move to start of line
132
+ // Adjust scroll
133
+ const visualRowIndex = this.findCurrentVisualRowIndex();
134
+ this.rowOffset = Math.max(0, visualRowIndex - Math.floor(this.screenRows / 2));
135
+ this.mode = 'edit';
136
+ this.setStatusMessage(`Jumped to line ${lineNumber}`, 1000);
137
+ }
138
+ /**
139
+ * Enters Go To Line mode.
140
+ */
141
+ function enterGoToLineMode() {
142
+ this.mode = 'goto_line';
143
+ this.goToLineQuery = '';
144
+ this.setStatusMessage('Go to Line (ESC to cancel): ');
145
+ }
124
146
  export const navigationMethods = {
125
147
  findCurrentVisualRowIndex,
126
148
  moveCursorLogically,
@@ -129,4 +151,6 @@ export const navigationMethods = {
129
151
  findVisualRowEnd,
130
152
  adjustCursorPosition,
131
153
  scroll,
154
+ jumpToLine,
155
+ enterGoToLineMode,
132
156
  };
@@ -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
- if (this.mode === 'search') {
127
- status = `SEARCH: ${this.searchQuery}`;
128
- }
129
- else {
130
- const visualRowIndex = this.findCurrentVisualRowIndex();
131
- const visualRow = this.visualRows[visualRowIndex];
132
- const visualX = visualRow ? (this.cursorX - visualRow.logicalXStart) : 0;
133
- const fileStatus = this.isDirty ? `* ${this.filepath}` : this.filepath;
134
- const pos = `Ln ${this.cursorY + 1}, Col ${this.cursorX + 1} (View: ${visualRowIndex + 1},${visualX + 1})`;
135
- const statusLeft = `[${fileStatus}]`.padEnd(Math.floor(contentWidth * 0.5));
136
- const statusRight = pos.padStart(Math.floor(contentWidth * 0.5));
137
- status = statusLeft + statusRight;
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
- const message = this.statusMessage.padEnd(contentWidth);
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
  }
@@ -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 search mode.
6
+ * Enters Find mode.
7
7
  */
8
- declare function enterSearchMode(this: CliEditor): void;
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
- enterSearchMode: typeof enterSearchMode;
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 {};