cliedit 0.1.1 → 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.
@@ -0,0 +1,21 @@
1
+ # Acknowledgements & Copyright (ACKNOWLEDGEMENTS)
2
+
3
+ `cliedit` is an open-source project that utilizes components (or modified/converted versions thereof) from other open-source projects. We express our deep gratitude to the communities and authors who provided the foundation for these components.
4
+
5
+ All components listed below are included as vendored code (integrated directly into the project source code) and have been adapted to fit **cliedit**'s TypeScript architecture and remove unnecessary features (e.g., mouse support).
6
+
7
+ ## Vendored Components List
8
+
9
+ **Original Project Name: `keypress`**
10
+
11
+ **Description & Origin:** Provides raw key event parsing logic for the TTY environment. The source code was converted from the original JavaScript version and optimized for Node.js.
12
+
13
+ **Repository:** https://github.com/TooTallNate/keypress
14
+
15
+ **License:** MIT
16
+
17
+ **Copyright Notes:**
18
+
19
+ - `keypress` was originally authored by **Nathan Rajlich** (tootallnate.net).
20
+ - Copyright (c) 2012 Nathan Rajlich.
21
+ - The module is based on the keypress logic found within the Node.js Core `readline` module.
@@ -20,7 +20,9 @@ export declare const KEYS: {
20
20
  CTRL_Q: string;
21
21
  CTRL_S: string;
22
22
  CTRL_W: string;
23
+ CTRL_R: string;
23
24
  CTRL_G: string;
25
+ CTRL_L: string;
24
26
  CTRL_Z: string;
25
27
  CTRL_Y: string;
26
28
  CTRL_K: string;
package/dist/constants.js CHANGED
@@ -22,7 +22,9 @@ export const KEYS = {
22
22
  CTRL_Q: '\x11', // Quit
23
23
  CTRL_S: '\x13', // Save
24
24
  CTRL_W: '\x17', // Find (Where is)
25
+ CTRL_R: '\x12', // Replace
25
26
  CTRL_G: '\x07', // Go to next
27
+ CTRL_L: '\x0c', // Go to Line (L)
26
28
  CTRL_Z: '\x1a', // Undo
27
29
  CTRL_Y: '\x19', // Redo
28
30
  CTRL_K: '\x0b', // Cut/Kill line
@@ -15,8 +15,11 @@ function setClipboard(text) {
15
15
  case 'win32':
16
16
  command = 'clip';
17
17
  break;
18
+ case 'linux': // <--- THÊM HỖ TRỢ LINUX
19
+ command = 'xclip -selection clipboard';
20
+ break;
18
21
  default:
19
- this.setStatusMessage('Clipboard only supported on macOS/Windows for now');
22
+ this.setStatusMessage('Clipboard not supported on this platform');
20
23
  return resolve();
21
24
  }
22
25
  const process = exec(command, (error) => {
@@ -43,8 +46,11 @@ function getClipboard() {
43
46
  case 'win32':
44
47
  command = 'powershell -command "Get-Clipboard"';
45
48
  break;
49
+ case 'linux': // <--- THÊM HỖ TRỢ LINUX
50
+ command = 'xclip -selection clipboard -o'; // -o (hoặc -out) để đọc
51
+ break;
46
52
  default:
47
- this.setStatusMessage('Clipboard only supported on macOS/Windows for now');
53
+ this.setStatusMessage('Clipboard not supported on this platform');
48
54
  return resolve('');
49
55
  }
50
56
  exec(command, (error, stdout) => {
package/dist/editor.d.ts CHANGED
@@ -42,8 +42,10 @@ export declare class CliEditor {
42
42
  statusTimeout: NodeJS.Timeout | null;
43
43
  isMessageCustom: boolean;
44
44
  quitConfirm: boolean;
45
- readonly DEFAULT_STATUS = "HELP: Ctrl+S = Save & Quit | Ctrl+Q = Quit | Ctrl+C = Copy All | Ctrl+Arrow = Select";
45
+ readonly DEFAULT_STATUS = "HELP: Ctrl+S = Save | Ctrl+Q = Quit | Ctrl+W = Find | Ctrl+R = Replace | Ctrl+L = Go to Line";
46
46
  searchQuery: string;
47
+ replaceQuery: string | null;
48
+ goToLineQuery: string;
47
49
  searchResults: {
48
50
  y: number;
49
51
  x: number;
@@ -18,6 +18,7 @@ declare function insertCharacter(this: CliEditor, char: string): void;
18
18
  declare function insertSoftTab(this: CliEditor): void;
19
19
  /**
20
20
  * Inserts a new line, splitting the current line at the cursor position.
21
+ * Implements auto-indent.
21
22
  */
22
23
  declare function insertNewLine(this: CliEditor): void;
23
24
  /**
@@ -51,14 +51,19 @@ function insertSoftTab() {
51
51
  }
52
52
  /**
53
53
  * Inserts a new line, splitting the current line at the cursor position.
54
+ * Implements auto-indent.
54
55
  */
55
56
  function insertNewLine() {
56
57
  const line = this.lines[this.cursorY] || '';
58
+ // Find indentation of the current line
59
+ const match = line.match(/^(\s*)/);
60
+ const indent = match ? match[1] : '';
57
61
  const remainder = line.slice(this.cursorX);
58
62
  this.lines[this.cursorY] = line.slice(0, this.cursorX);
59
- this.lines.splice(this.cursorY + 1, 0, remainder);
63
+ // Add new line with the same indentation + remainder
64
+ this.lines.splice(this.cursorY + 1, 0, indent + remainder);
60
65
  this.cursorY++;
61
- this.cursorX = 0;
66
+ this.cursorX = indent.length; // Move cursor to end of indent
62
67
  this.setDirty();
63
68
  }
64
69
  /**
package/dist/editor.js CHANGED
@@ -13,7 +13,7 @@ import { historyMethods } from './editor.history.js';
13
13
  import { ioMethods } from './editor.io.js';
14
14
  import { keyHandlingMethods } from './editor.keys.js';
15
15
  import { selectionMethods } from './editor.selection.js';
16
- const DEFAULT_STATUS = 'HELP: Ctrl+S = Save & Quit | Ctrl+Q = Quit | Ctrl+C = Copy All | Ctrl+Arrow = Select';
16
+ const DEFAULT_STATUS = 'HELP: Ctrl+S = Save | Ctrl+Q = Quit | Ctrl+W = Find | Ctrl+R = Replace | Ctrl+L = Go to Line';
17
17
  /**
18
18
  * Main editor class managing application state, TTY interaction, and rendering.
19
19
  */
@@ -36,6 +36,8 @@ export class CliEditor {
36
36
  this.quitConfirm = false;
37
37
  this.DEFAULT_STATUS = DEFAULT_STATUS;
38
38
  this.searchQuery = '';
39
+ this.replaceQuery = null; // null = Find mode, string = Replace mode
40
+ this.goToLineQuery = ''; // For Go to Line prompt
39
41
  this.searchResults = [];
40
42
  this.searchResultIndex = -1;
41
43
  this.isCleanedUp = false;
@@ -3,6 +3,8 @@ export type TKeyHandlingMethods = {
3
3
  handleKeypressEvent: (ch: string, key: KeypressEvent) => void;
4
4
  handleEditKeys: (key: string) => boolean;
5
5
  handleSearchKeys: (key: string) => void;
6
+ handleSearchConfirmKeys: (key: string) => void;
7
+ handleGoToLineKeys: (key: string) => void;
6
8
  handleCtrlQ: () => void;
7
9
  handleCopy: () => Promise<void>;
8
10
  handleCharacterKey: (ch: string) => void;
@@ -13,10 +13,21 @@ function handleKeypressEvent(ch, key) {
13
13
  // --- 1. Xử lý trường hợp key là null/undefined (Ký tự in được) ---
14
14
  if (!key) {
15
15
  if (ch && ch.length === 1 && ch >= ' ' && ch <= '~') {
16
- edited = this.handleEditKeys(ch);
17
- if (edited) {
18
- this.saveState();
19
- this.recalculateVisualRows(); // Phải tính toán lại sau khi gõ
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);
20
31
  }
21
32
  this.render();
22
33
  return;
@@ -67,9 +78,15 @@ function handleKeypressEvent(ch, key) {
67
78
  keyName = key.sequence;
68
79
  }
69
80
  // --- 3. Định tuyến theo Mode ---
70
- if (this.mode === 'search') {
81
+ if (this.mode === 'search_find' || this.mode === 'search_replace') {
71
82
  this.handleSearchKeys(keyName || ch);
72
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
+ }
73
90
  else {
74
91
  // 4. Xử lý phím lựa chọn (Ctrl+Arrow) - Navigation
75
92
  switch (keyName) {
@@ -174,7 +191,13 @@ function handleEditKeys(key) {
174
191
  return true;
175
192
  // --- Search & History ---
176
193
  case KEYS.CTRL_W:
177
- 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();
178
201
  return false;
179
202
  case KEYS.CTRL_G:
180
203
  this.findNext();
@@ -268,28 +291,130 @@ async function handleSave() {
268
291
  }
269
292
  }
270
293
  /**
271
- * Handles Search Mode input keys.
294
+ * Handles Search Mode input keys (for 'search_find' and 'search_replace').
272
295
  */
273
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
+ };
274
305
  switch (key) {
275
306
  case KEYS.ENTER:
276
- this.executeSearch();
277
- 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
278
367
  this.findNext();
279
368
  break;
369
+ case 'a': // All
370
+ this.replaceAll();
371
+ break;
372
+ case 'q': // Quit
280
373
  case KEYS.ESCAPE:
281
374
  case KEYS.CTRL_C:
282
375
  case KEYS.CTRL_Q:
283
376
  this.mode = 'edit';
284
- this.searchQuery = '';
285
- 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();
286
408
  break;
287
409
  case KEYS.BACKSPACE:
288
- this.searchQuery = this.searchQuery.slice(0, -1);
410
+ this.goToLineQuery = this.goToLineQuery.slice(0, -1);
411
+ this.setStatusMessage('Go to Line: ' + this.goToLineQuery);
289
412
  break;
290
413
  default:
291
- if (key.length === 1 && key >= ' ' && key <= '~') {
292
- 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);
293
418
  }
294
419
  }
295
420
  }
@@ -297,6 +422,8 @@ export const keyHandlingMethods = {
297
422
  handleKeypressEvent,
298
423
  handleEditKeys,
299
424
  handleSearchKeys,
425
+ handleSearchConfirmKeys,
426
+ handleGoToLineKeys,
300
427
  handleCtrlQ,
301
428
  handleCopy,
302
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 {};
@@ -1,16 +1,28 @@
1
1
  // src/editor.search.ts
2
2
  /**
3
- * Methods related to Find/Search functionality.
3
+ * Methods related to Find/Search/Replace functionality.
4
4
  */
5
5
  /**
6
- * Enters search mode.
6
+ * Enters Find mode.
7
7
  */
8
- function enterSearchMode() {
9
- this.mode = 'search';
8
+ function enterFindMode() {
9
+ this.mode = 'search_find';
10
10
  this.searchQuery = '';
11
+ this.replaceQuery = null; // Mark as Find-Only
11
12
  this.searchResults = [];
12
13
  this.searchResultIndex = -1;
13
- this.setStatusMessage('Search (ESC/Ctrl+Q/C to cancel, ENTER to find): ');
14
+ this.setStatusMessage('Find (ESC to cancel): ');
15
+ }
16
+ /**
17
+ * Enters Replace mode (starting with the "Find" prompt).
18
+ */
19
+ function enterReplaceMode() {
20
+ this.mode = 'search_find';
21
+ this.searchQuery = '';
22
+ this.replaceQuery = ''; // Mark as Replace flow
23
+ this.searchResults = [];
24
+ this.searchResultIndex = -1;
25
+ this.setStatusMessage('Find (for Replace): ');
14
26
  }
15
27
  /**
16
28
  * Executes the search and populates results.
@@ -27,18 +39,114 @@ function executeSearch() {
27
39
  }
28
40
  }
29
41
  this.searchResultIndex = -1;
30
- this.setStatusMessage(`Found ${this.searchResults.length} results for "${this.searchQuery}"`);
42
+ if (this.replaceQuery === null) { // Find-only flow
43
+ this.setStatusMessage(`Found ${this.searchResults.length} results for "${this.searchQuery}"`);
44
+ }
31
45
  }
32
46
  /**
33
47
  * Jumps to the next search result.
34
48
  */
35
49
  function findNext() {
50
+ if (this.searchQuery === '') {
51
+ this.enterFindMode();
52
+ return;
53
+ }
54
+ // Execute search if results are not yet populated
55
+ if (this.searchResults.length === 0 && this.searchResultIndex === -1) {
56
+ this.executeSearch();
57
+ }
36
58
  if (this.searchResults.length === 0) {
37
- this.setStatusMessage('No search results');
59
+ this.setStatusMessage('No results found');
60
+ this.mode = 'edit';
38
61
  return;
39
62
  }
40
- this.searchResultIndex = (this.searchResultIndex + 1) % this.searchResults.length;
63
+ this.searchResultIndex++;
64
+ if (this.searchResultIndex >= this.searchResults.length) {
65
+ this.setStatusMessage('End of file reached. Starting from top.');
66
+ this.searchResultIndex = 0;
67
+ }
68
+ const result = this.searchResults[this.searchResultIndex];
69
+ this.jumpToResult(result);
70
+ if (this.replaceQuery !== null) {
71
+ // Replace flow: Enter confirmation step
72
+ this.mode = 'search_confirm';
73
+ this.setStatusMessage(`Replace "${this.searchQuery}"? (y/n/a/q)`);
74
+ }
75
+ else {
76
+ // Find-only flow: Go back to edit
77
+ this.mode = 'edit';
78
+ }
79
+ }
80
+ /**
81
+ * Replaces the current highlighted search result and finds the next one.
82
+ */
83
+ function replaceCurrentAndFindNext() {
84
+ if (this.searchResultIndex === -1 || !this.searchResults[this.searchResultIndex]) {
85
+ this.findNext();
86
+ return;
87
+ }
88
+ const result = this.searchResults[this.searchResultIndex];
89
+ const line = this.lines[result.y];
90
+ const before = line.substring(0, result.x);
91
+ const after = line.substring(result.x + this.searchQuery.length);
92
+ // Use replaceQuery (it's guaranteed to be a string here, not null)
93
+ this.lines[result.y] = before + this.replaceQuery + after;
94
+ this.setDirty();
95
+ // Store current position to find the *next* match after this one
96
+ const replacedResultY = result.y;
97
+ const replacedResultX = result.x;
98
+ // We MUST re-execute search as all indices may have changed
99
+ this.executeSearch();
100
+ this.recalculateVisualRows();
101
+ // Find the next result *after* the one we just replaced
102
+ let nextIndex = -1;
103
+ for (let i = 0; i < this.searchResults.length; i++) {
104
+ const res = this.searchResults[i];
105
+ if (res.y > replacedResultY || (res.y === replacedResultY && res.x > replacedResultX)) {
106
+ nextIndex = i;
107
+ break;
108
+ }
109
+ }
110
+ if (nextIndex === -1) {
111
+ this.setStatusMessage('No more results');
112
+ this.mode = 'edit';
113
+ this.searchResultIndex = -1; // Reset search
114
+ return;
115
+ }
116
+ // Found the next one
117
+ this.searchResultIndex = nextIndex;
41
118
  this.jumpToResult(this.searchResults[this.searchResultIndex]);
119
+ this.mode = 'search_confirm'; // Stay in confirm mode
120
+ this.setStatusMessage(`Replace "${this.searchQuery}"? (y/n/a/q)`);
121
+ }
122
+ /**
123
+ * Replaces all occurrences of the search query.
124
+ */
125
+ function replaceAll() {
126
+ if (this.searchResults.length === 0) {
127
+ this.executeSearch();
128
+ }
129
+ if (this.searchResults.length === 0) {
130
+ this.setStatusMessage('No results found');
131
+ this.mode = 'edit';
132
+ return;
133
+ }
134
+ let count = 0;
135
+ // Iterate backwards to ensure indices remain valid during replacement
136
+ for (let i = this.searchResults.length - 1; i >= 0; i--) {
137
+ const result = this.searchResults[i];
138
+ const line = this.lines[result.y];
139
+ const before = line.substring(0, result.x);
140
+ const after = line.substring(result.x + this.searchQuery.length);
141
+ this.lines[result.y] = before + this.replaceQuery + after;
142
+ count++;
143
+ }
144
+ this.setDirty();
145
+ this.recalculateVisualRows();
146
+ this.mode = 'edit';
147
+ this.searchResults = [];
148
+ this.searchResultIndex = -1;
149
+ this.setStatusMessage(`Replaced ${count} occurrences.`);
42
150
  }
43
151
  /**
44
152
  * Moves cursor and adjusts scroll offset to make the result visible.
@@ -51,8 +159,11 @@ function jumpToResult(result) {
51
159
  this.rowOffset = Math.max(0, visualRowIndex - Math.floor(this.screenRows / 2));
52
160
  }
53
161
  export const searchMethods = {
54
- enterSearchMode,
162
+ enterFindMode,
163
+ enterReplaceMode,
55
164
  executeSearch,
56
165
  findNext,
166
+ replaceCurrentAndFindNext,
167
+ replaceAll,
57
168
  jumpToResult,
58
169
  };
package/dist/types.d.ts CHANGED
@@ -15,4 +15,4 @@ export interface VisualRow {
15
15
  logicalXStart: number;
16
16
  content: string;
17
17
  }
18
- export type EditorMode = 'edit' | 'search';
18
+ export type EditorMode = 'edit' | 'search_find' | 'search_replace' | 'search_confirm' | 'goto_line';
@@ -142,6 +142,16 @@ function emitKey(stream, s) {
142
142
  key.name = parts[1].toLowerCase();
143
143
  key.meta = true;
144
144
  key.shift = /^[A-Z]$/.test(parts[1]);
145
+ // ***** START BUG FIX *****
146
+ // The original library failed to handle any standard printable
147
+ // characters (numbers, symbols) that weren't a-z or A-Z.
148
+ }
149
+ else if (s.length === 1 && s >= ' ' && s <= '~') {
150
+ // Standard printable character (digits, symbols, etc.)
151
+ key.name = s;
152
+ // We can infer shift status for common symbols
153
+ key.shift = '!@#$%^&*()_+{}|:"<>?~'.includes(s);
154
+ // ***** END BUG FIX *****
145
155
  }
146
156
  else if ((parts = functionKeyCodeRe.exec(s))) {
147
157
  // ansi escape sequence
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cliedit",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "A lightweight, raw-mode terminal editor utility for Node.js CLI applications, with line wrapping and undo/redo support.",
5
5
  "repository": "https://github.com/CodeTease/cliedit",
6
6
  "type": "module",
@@ -15,7 +15,8 @@
15
15
  "files": [
16
16
  "dist",
17
17
  "LICENSE",
18
- "README.md"
18
+ "README.md",
19
+ "ACKNOWLEDGEMENTS.md"
19
20
  ],
20
21
  "scripts": {
21
22
  "build": "tsc",