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.
@@ -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';
@@ -0,0 +1,17 @@
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.
4
+ */
5
+ export interface KeypressEvent {
6
+ name?: string;
7
+ ctrl: boolean;
8
+ meta: boolean;
9
+ shift: boolean;
10
+ sequence: string;
11
+ code?: string;
12
+ }
13
+ /**
14
+ * Hàm chính, chấp nhận một Readable Stream và làm cho nó
15
+ * phát ra sự kiện "keypress".
16
+ */
17
+ export default function keypress(stream: NodeJS.ReadStream): void;
@@ -0,0 +1,435 @@
1
+ // src/vendor/keypress.ts
2
+ // Đây là phiên bản "vendored" của thư viện 'keypress' (0.2.1)
3
+ // được chuyển đổi sang TypeScript và loại bỏ hỗ trợ chuột
4
+ // để tích hợp trực tiếp vào cliedit.
5
+ import { EventEmitter } from 'events';
6
+ import { StringDecoder } from 'string_decoder';
7
+ /**
8
+ * Hàm polyfill cho `EventEmitter.listenerCount()`, để tương thích ngược.
9
+ */
10
+ let listenerCount = EventEmitter.listenerCount;
11
+ if (!listenerCount) {
12
+ listenerCount = function (emitter, event) {
13
+ return emitter.listeners(event).length;
14
+ };
15
+ }
16
+ /**
17
+ * Regexes dùng để phân tích escape code của ansi
18
+ */
19
+ const metaKeyCodeRe = /^(?:\x1b)([a-zA-Z0-9])$/;
20
+ const functionKeyCodeRe = /^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/;
21
+ /**
22
+ * Hàm chính, chấp nhận một Readable Stream và làm cho nó
23
+ * phát ra sự kiện "keypress".
24
+ */
25
+ export default function keypress(stream) {
26
+ if (isEmittingKeypress(stream))
27
+ return;
28
+ // Gắn decoder vào stream để theo dõi
29
+ stream._keypressDecoder = new StringDecoder('utf8');
30
+ function onData(b) {
31
+ if (listenerCount(stream, 'keypress') > 0) {
32
+ const r = stream._keypressDecoder.write(b);
33
+ if (r)
34
+ emitKey(stream, r);
35
+ }
36
+ else {
37
+ // Không ai đang nghe, gỡ bỏ listener
38
+ stream.removeListener('data', onData);
39
+ stream.on('newListener', onNewListener);
40
+ }
41
+ }
42
+ function onNewListener(event) {
43
+ if (event === 'keypress') {
44
+ stream.on('data', onData);
45
+ stream.removeListener('newListener', onNewListener);
46
+ }
47
+ }
48
+ if (listenerCount(stream, 'keypress') > 0) {
49
+ stream.on('data', onData);
50
+ }
51
+ else {
52
+ stream.on('newListener', onNewListener);
53
+ }
54
+ }
55
+ /**
56
+ * Kiểm tra xem stream đã phát ra sự kiện "keypress" hay chưa.
57
+ */
58
+ function isEmittingKeypress(stream) {
59
+ let rtn = !!stream._keypressDecoder;
60
+ if (!rtn) {
61
+ // XXX: Đối với các phiên bản node cũ, chúng ta muốn xóa các
62
+ // listener "data" và "newListener" hiện có vì chúng sẽ không
63
+ // bao gồm các phần mở rộng của module này (như "mousepress" đã bị loại bỏ).
64
+ stream.listeners('data').slice(0).forEach(function (l) {
65
+ if (l.name === 'onData' && /emitKey/.test(l.toString())) {
66
+ // FIX TS2769: Ép kiểu 'l' thành kiểu listener hợp lệ
67
+ stream.removeListener('data', l);
68
+ }
69
+ });
70
+ stream.listeners('newListener').slice(0).forEach(function (l) {
71
+ if (l.name === 'onNewListener' && /keypress/.test(l.toString())) {
72
+ // FIX TS2769: Ép kiểu 'l' thành kiểu listener hợp lệ
73
+ stream.removeListener('newListener', l);
74
+ }
75
+ });
76
+ }
77
+ return rtn;
78
+ }
79
+ /**
80
+ * Phần code bên dưới được lấy từ module `readline.js` của node-core
81
+ * và đã được chuyển đổi sang TypeScript.
82
+ */
83
+ function emitKey(stream, s) {
84
+ let ch;
85
+ const key = {
86
+ name: undefined,
87
+ ctrl: false,
88
+ meta: false,
89
+ shift: false,
90
+ sequence: s,
91
+ };
92
+ let parts;
93
+ // Cảnh báo: Block `Buffer.isBuffer(s)` đã bị loại bỏ.
94
+ // Lý do: `onData` luôn gọi `emitKey` với một string (kết quả từ StringDecoder).
95
+ // Block đệ quy (paste) cũng gọi với string.
96
+ // Vì vậy, `s` luôn là string.
97
+ if (s === '\r') {
98
+ // carriage return
99
+ key.name = 'return';
100
+ }
101
+ else if (s === '\n') {
102
+ // enter, đáng lẽ phải là linefeed
103
+ key.name = 'enter';
104
+ }
105
+ else if (s === '\t') {
106
+ // tab
107
+ key.name = 'tab';
108
+ }
109
+ else if (s === '\b' ||
110
+ s === '\x7f' ||
111
+ s === '\x1b\x7f' ||
112
+ s === '\x1b\b') {
113
+ // backspace hoặc ctrl+h
114
+ key.name = 'backspace';
115
+ key.meta = s.charAt(0) === '\x1b';
116
+ }
117
+ else if (s === '\x1b' || s === '\x1b\x1b') {
118
+ // escape key
119
+ key.name = 'escape';
120
+ key.meta = s.length === 2;
121
+ }
122
+ else if (s === ' ' || s === '\x1b ') {
123
+ key.name = 'space';
124
+ key.meta = s.length === 2;
125
+ }
126
+ else if (s <= '\x1a') {
127
+ // ctrl+letter
128
+ key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
129
+ key.ctrl = true;
130
+ }
131
+ else if (s.length === 1 && s >= 'a' && s <= 'z') {
132
+ // lowercase letter
133
+ key.name = s;
134
+ }
135
+ else if (s.length === 1 && s >= 'A' && s <= 'Z') {
136
+ // shift+letter
137
+ key.name = s.toLowerCase();
138
+ key.shift = true;
139
+ }
140
+ else if ((parts = metaKeyCodeRe.exec(s))) {
141
+ // meta+character key
142
+ key.name = parts[1].toLowerCase();
143
+ key.meta = true;
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 *****
155
+ }
156
+ else if ((parts = functionKeyCodeRe.exec(s))) {
157
+ // ansi escape sequence
158
+ // Lắp ráp lại key code, bỏ qua \x1b đứng đầu,
159
+ // bitflag của phím bổ trợ và bất kỳ chuỗi "1;" vô nghĩa nào
160
+ const code = (parts[1] || '') +
161
+ (parts[2] || '') +
162
+ (parts[4] || '') +
163
+ (parts[6] || '');
164
+ // FIX TS2362: Chuyển đổi (parts[...]) sang number bằng parseInt
165
+ const modifier = parseInt(parts[3] || parts[5] || '1', 10) - 1;
166
+ // Phân tích phím bổ trợ
167
+ key.ctrl = !!(modifier & 4);
168
+ key.meta = !!(modifier & 10);
169
+ key.shift = !!(modifier & 1);
170
+ key.code = code;
171
+ // Phân tích chính phím đó
172
+ switch (code) {
173
+ /* xterm/gnome ESC O letter */
174
+ case 'OP':
175
+ key.name = 'f1';
176
+ break;
177
+ case 'OQ':
178
+ key.name = 'f2';
179
+ break;
180
+ case 'OR':
181
+ key.name = 'f3';
182
+ break;
183
+ case 'OS':
184
+ key.name = 'f4';
185
+ break;
186
+ /* xterm/rxvt ESC [ number ~ */
187
+ case '[11~':
188
+ key.name = 'f1';
189
+ break;
190
+ case '[12~':
191
+ key.name = 'f2';
192
+ break;
193
+ case '[13~':
194
+ key.name = 'f3';
195
+ break;
196
+ case '[14~':
197
+ key.name = 'f4';
198
+ break;
199
+ /* from Cygwin and used in libuv */
200
+ case '[[A':
201
+ key.name = 'f1';
202
+ break;
203
+ case '[[B':
204
+ key.name = 'f2';
205
+ break;
206
+ case '[[C':
207
+ key.name = 'f3';
208
+ break;
209
+ case '[[D':
210
+ key.name = 'f4';
211
+ break;
212
+ case '[[E':
213
+ key.name = 'f5';
214
+ break;
215
+ /* common */
216
+ case '[15~':
217
+ key.name = 'f5';
218
+ break;
219
+ case '[17~':
220
+ key.name = 'f6';
221
+ break;
222
+ case '[18~':
223
+ key.name = 'f7';
224
+ break;
225
+ case '[19~':
226
+ key.name = 'f8';
227
+ break;
228
+ case '[20~':
229
+ key.name = 'f9';
230
+ break;
231
+ case '[21~':
232
+ key.name = 'f10';
233
+ break;
234
+ case '[23~':
235
+ key.name = 'f11';
236
+ break;
237
+ case '[24~':
238
+ key.name = 'f12';
239
+ break;
240
+ /* xterm ESC [ letter */
241
+ case '[A':
242
+ key.name = 'up';
243
+ break;
244
+ case '[B':
245
+ key.name = 'down';
246
+ break;
247
+ case '[C':
248
+ key.name = 'right';
249
+ break;
250
+ case '[D':
251
+ key.name = 'left';
252
+ break;
253
+ case '[E':
254
+ key.name = 'clear';
255
+ break;
256
+ case '[F':
257
+ key.name = 'end';
258
+ break;
259
+ case '[H':
260
+ key.name = 'home';
261
+ break;
262
+ /* xterm/gnome ESC O letter */
263
+ case 'OA':
264
+ key.name = 'up';
265
+ break;
266
+ case 'OB':
267
+ key.name = 'down';
268
+ break;
269
+ case 'OC':
270
+ key.name = 'right';
271
+ break;
272
+ case 'OD':
273
+ key.name = 'left';
274
+ break;
275
+ case 'OE':
276
+ key.name = 'clear';
277
+ break;
278
+ case 'OF':
279
+ key.name = 'end';
280
+ break;
281
+ case 'OH':
282
+ key.name = 'home';
283
+ break;
284
+ /* xterm/rxvt ESC [ number ~ */
285
+ case '[1~':
286
+ key.name = 'home';
287
+ break;
288
+ case '[2~':
289
+ key.name = 'insert';
290
+ break;
291
+ case '[3~':
292
+ key.name = 'delete';
293
+ break;
294
+ case '[4~':
295
+ key.name = 'end';
296
+ break;
297
+ case '[5~':
298
+ key.name = 'pageup';
299
+ break;
300
+ case '[6~':
301
+ key.name = 'pagedown';
302
+ break;
303
+ /* putty */
304
+ case '[[5~':
305
+ key.name = 'pageup';
306
+ break;
307
+ case '[[6~':
308
+ key.name = 'pagedown';
309
+ break;
310
+ /* rxvt */
311
+ case '[7~':
312
+ key.name = 'home';
313
+ break;
314
+ case '[8~':
315
+ key.name = 'end';
316
+ break;
317
+ /* rxvt keys with modifiers */
318
+ case '[a':
319
+ key.name = 'up';
320
+ key.shift = true;
321
+ break;
322
+ case '[b':
323
+ key.name = 'down';
324
+ key.shift = true;
325
+ break;
326
+ case '[c':
327
+ key.name = 'right';
328
+ key.shift = true;
329
+ break;
330
+ case '[d':
331
+ key.name = 'left';
332
+ key.shift = true;
333
+ break;
334
+ case '[e':
335
+ key.name = 'clear';
336
+ key.shift = true;
337
+ break;
338
+ case '[2$':
339
+ key.name = 'insert';
340
+ key.shift = true;
341
+ break;
342
+ case '[3$':
343
+ key.name = 'delete';
344
+ key.shift = true;
345
+ break;
346
+ case '[5$':
347
+ key.name = 'pageup';
348
+ key.shift = true;
349
+ break;
350
+ case '[6$':
351
+ key.name = 'pagedown';
352
+ key.shift = true;
353
+ break;
354
+ case '[7$':
355
+ key.name = 'home';
356
+ key.shift = true;
357
+ break;
358
+ case '[8$':
359
+ key.name = 'end';
360
+ key.shift = true;
361
+ break;
362
+ case 'Oa':
363
+ key.name = 'up';
364
+ key.ctrl = true;
365
+ break;
366
+ case 'Ob':
367
+ key.name = 'down';
368
+ key.ctrl = true;
369
+ break;
370
+ case 'Oc':
371
+ key.name = 'right';
372
+ key.ctrl = true;
373
+ break;
374
+ case 'Od':
375
+ key.name = 'left';
376
+ key.ctrl = true;
377
+ break;
378
+ case 'Oe':
379
+ key.name = 'clear';
380
+ key.ctrl = true;
381
+ break;
382
+ case '[2^':
383
+ key.name = 'insert';
384
+ key.ctrl = true;
385
+ break;
386
+ case '[3^':
387
+ key.name = 'delete';
388
+ key.ctrl = true;
389
+ break;
390
+ case '[5^':
391
+ key.name = 'pageup';
392
+ key.ctrl = true;
393
+ break;
394
+ case '[6^':
395
+ key.name = 'pagedown';
396
+ key.ctrl = true;
397
+ break;
398
+ case '[7^':
399
+ key.name = 'home';
400
+ key.ctrl = true;
401
+ break;
402
+ case '[8^':
403
+ key.name = 'end';
404
+ key.ctrl = true;
405
+ break;
406
+ /* misc. */
407
+ case '[Z':
408
+ key.name = 'tab';
409
+ key.shift = true;
410
+ break;
411
+ default:
412
+ key.name = 'undefined';
413
+ break;
414
+ }
415
+ }
416
+ else if (s.length > 1 && s[0] !== '\x1b') {
417
+ // Nhận được một chuỗi ký tự dài hơn một.
418
+ // Có thể là paste, vì nó không phải là control sequence.
419
+ for (const c of s) {
420
+ emitKey(stream, c);
421
+ }
422
+ return;
423
+ }
424
+ // XXX: code phân tích "mouse" đã bị XÓA theo yêu cầu.
425
+ // Không phát ra key nếu không tìm thấy tên
426
+ if (key.name === undefined) {
427
+ return; // key = undefined;
428
+ }
429
+ if (s.length === 1) {
430
+ ch = s;
431
+ }
432
+ if (key || ch) {
433
+ stream.emit('keypress', ch, key);
434
+ }
435
+ }