cliedit 0.1.3 → 0.3.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 +41 -12
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/editor.d.ts +10 -2
- package/dist/editor.editing.d.ts +1 -1
- package/dist/editor.editing.js +3 -2
- package/dist/editor.io.js +1 -0
- package/dist/editor.js +36 -14
- package/dist/editor.keys.d.ts +1 -0
- package/dist/editor.keys.js +86 -1
- package/dist/editor.navigation.d.ts +4 -0
- package/dist/editor.navigation.js +91 -0
- package/dist/editor.rendering.js +31 -3
- package/dist/editor.search.d.ts +5 -0
- package/dist/editor.search.js +16 -0
- package/dist/editor.swap.d.ts +17 -0
- package/dist/editor.swap.js +83 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +48 -10
- package/dist/types.d.ts +5 -0
- package/dist/vendor/keypress.js +27 -1
- package/package.json +7 -8
package/README.md
CHANGED
|
@@ -18,6 +18,9 @@ It includes line wrapping, visual navigation, smart auto-indentation, undo/redo,
|
|
|
18
18
|
- **Search & Replace:** `Ctrl+W` to find text, `Ctrl+R` to find and replace interactively.
|
|
19
19
|
- **Go to Line:** `Ctrl+L` to quickly jump to a specific line number.
|
|
20
20
|
- **Smart Auto-Indentation:** Automatically preserves indentation level when pressing Enter.
|
|
21
|
+
- **Smart Navigation:** `Alt + Left/Right` to jump by words, `Ctrl + M` to jump between matching brackets.
|
|
22
|
+
- **Piping Support:** Works with standard Unix pipes (e.g. `cat file.txt | cliedit`).
|
|
23
|
+
- **Crash Recovery:** Automatically saves changes to a hidden swap file (e.g. `.filename.swp`) to prevent data loss.
|
|
21
24
|
|
|
22
25
|
## Installation
|
|
23
26
|
```bash
|
|
@@ -34,35 +37,61 @@ import path from 'path';
|
|
|
34
37
|
|
|
35
38
|
async function getCommitMessage() {
|
|
36
39
|
const tempFile = path.resolve(process.cwd(), 'COMMIT_MSG.txt');
|
|
37
|
-
|
|
40
|
+
|
|
41
|
+
// Example with custom options
|
|
42
|
+
const options = {
|
|
43
|
+
tabSize: 2,
|
|
44
|
+
gutterWidth: 3
|
|
45
|
+
};
|
|
38
46
|
|
|
39
47
|
try {
|
|
40
|
-
const result = await openEditor(tempFile);
|
|
41
|
-
|
|
42
|
-
// Give the terminal a moment to restore
|
|
43
|
-
await new Promise(res => setTimeout(res, 50));
|
|
48
|
+
const result = await openEditor(tempFile, options);
|
|
44
49
|
|
|
45
50
|
if (result.saved) {
|
|
46
|
-
console.log('Message saved
|
|
47
|
-
console.log('---------------------');
|
|
48
|
-
console.log(result.content);
|
|
49
|
-
console.log('---------------------');
|
|
51
|
+
console.log('Message saved:', result.content);
|
|
50
52
|
} else {
|
|
51
53
|
console.log('Editor quit without saving.');
|
|
52
54
|
}
|
|
53
55
|
} catch (err) {
|
|
54
|
-
console.error('Editor failed
|
|
56
|
+
console.error('Editor failed:', err);
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
getCommitMessage();
|
|
59
61
|
```
|
|
60
62
|
|
|
63
|
+
### Piping Support
|
|
64
|
+
|
|
65
|
+
`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
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Edit a file using cat
|
|
69
|
+
cat README.md | node my-app.js
|
|
70
|
+
|
|
71
|
+
# Edit the output of a command
|
|
72
|
+
git diff | node my-app.js
|
|
73
|
+
```
|
|
74
|
+
|
|
61
75
|
## Public API
|
|
62
76
|
|
|
63
|
-
`openEditor(filepath: string)`
|
|
77
|
+
`openEditor(filepath: string, options?: EditorOptions)`
|
|
78
|
+
|
|
79
|
+
Opens the editor for the specified file.
|
|
80
|
+
|
|
81
|
+
- **filepath**: Path to the file to edit.
|
|
82
|
+
- **options**: (Optional) Configuration object.
|
|
83
|
+
- `tabSize`: Number of spaces for a tab (default: 4).
|
|
84
|
+
- `gutterWidth`: Width of the line number gutter (default: 5).
|
|
85
|
+
|
|
86
|
+
- **Returns:** `Promise<{ saved: boolean; content: string }>`
|
|
87
|
+
* `saved`: `true` if the user saved (Ctrl+S), `false` otherwise (Ctrl+Q).
|
|
88
|
+
* `content`: The final content of the file as a string.
|
|
89
|
+
|
|
90
|
+
### Crash Recovery
|
|
91
|
+
|
|
92
|
+
`cliedit` includes a built-in safety mechanism. It periodically saves the current content to a hidden swap file (e.g., `.myfile.txt.swp`) in the same directory.
|
|
64
93
|
|
|
65
|
-
|
|
94
|
+
If the process crashes or is terminated abruptly, the next time you open the file, `cliedit` will detect the swap file and automatically recover the unsaved content, displaying a `RECOVERED FROM SWAP FILE` message.
|
|
66
95
|
|
|
67
96
|
- **Returns:** `Promise<{ saved: boolean; content: string }>`
|
|
68
97
|
* `saved`: `true` if the user saved (Ctrl+S), `false` otherwise (Ctrl+Q).
|
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
|
@@ -31,6 +31,7 @@ export const KEYS = {
|
|
|
31
31
|
CTRL_U: '\x15', // Paste/Un-kill
|
|
32
32
|
CTRL_X: '\x18', // Cut Selection
|
|
33
33
|
CTRL_V: '\x16', // Paste Selection
|
|
34
|
+
CTRL_M: '\x0d', // Match Bracket (Ctrl+M is often Enter, but we distinguish if possible or rely on context)
|
|
34
35
|
// Selection Keys (Mapped to Ctrl+Arrow for reliable detection)
|
|
35
36
|
CTRL_ARROW_UP: 'C-up',
|
|
36
37
|
CTRL_ARROW_DOWN: 'C-down',
|
package/dist/editor.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HistoryManager } from './history.js';
|
|
2
|
-
import { VisualRow, EditorMode } from './types.js';
|
|
2
|
+
import { VisualRow, EditorMode, EditorOptions } from './types.js';
|
|
3
|
+
import { SwapManager } from './editor.swap.js';
|
|
3
4
|
import { editingMethods } from './editor.editing.js';
|
|
4
5
|
import { clipboardMethods } from './editor.clipboard.js';
|
|
5
6
|
import { navigationMethods } from './editor.navigation.js';
|
|
@@ -35,6 +36,7 @@ export declare class CliEditor {
|
|
|
35
36
|
screenRows: number;
|
|
36
37
|
screenCols: number;
|
|
37
38
|
gutterWidth: number;
|
|
39
|
+
tabSize: number;
|
|
38
40
|
screenStartRow: number;
|
|
39
41
|
visualRows: VisualRow[];
|
|
40
42
|
mode: EditorMode;
|
|
@@ -50,16 +52,22 @@ export declare class CliEditor {
|
|
|
50
52
|
y: number;
|
|
51
53
|
x: number;
|
|
52
54
|
}[];
|
|
55
|
+
searchResultMap: Map<number, Array<{
|
|
56
|
+
start: number;
|
|
57
|
+
end: number;
|
|
58
|
+
}>>;
|
|
53
59
|
searchResultIndex: number;
|
|
54
60
|
history: HistoryManager;
|
|
61
|
+
swapManager: SwapManager;
|
|
55
62
|
isCleanedUp: boolean;
|
|
56
63
|
resolvePromise: ((value: {
|
|
57
64
|
saved: boolean;
|
|
58
65
|
content: string;
|
|
59
66
|
}) => void) | null;
|
|
60
67
|
rejectPromise: ((reason?: any) => void) | null;
|
|
68
|
+
inputStream: any;
|
|
61
69
|
isExiting: boolean;
|
|
62
|
-
constructor(initialContent: string, filepath: string);
|
|
70
|
+
constructor(initialContent: string, filepath: string, options?: EditorOptions);
|
|
63
71
|
run(): Promise<{
|
|
64
72
|
saved: boolean;
|
|
65
73
|
content: string;
|
package/dist/editor.editing.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ declare function insertContentAtCursor(this: CliEditor, contentLines: string[]):
|
|
|
13
13
|
*/
|
|
14
14
|
declare function insertCharacter(this: CliEditor, char: string): void;
|
|
15
15
|
/**
|
|
16
|
-
* Inserts a soft tab (
|
|
16
|
+
* Inserts a soft tab (using configured tabSize).
|
|
17
17
|
*/
|
|
18
18
|
declare function insertSoftTab(this: CliEditor): void;
|
|
19
19
|
/**
|
package/dist/editor.editing.js
CHANGED
|
@@ -44,10 +44,11 @@ function insertCharacter(char) {
|
|
|
44
44
|
this.cursorX += char.length;
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
|
-
* Inserts a soft tab (
|
|
47
|
+
* Inserts a soft tab (using configured tabSize).
|
|
48
48
|
*/
|
|
49
49
|
function insertSoftTab() {
|
|
50
|
-
this.
|
|
50
|
+
const spaces = ' '.repeat(this.tabSize || 4);
|
|
51
|
+
this.insertCharacter(spaces);
|
|
51
52
|
}
|
|
52
53
|
/**
|
|
53
54
|
* Inserts a new line, splitting the current line at the cursor position.
|
package/dist/editor.io.js
CHANGED
|
@@ -16,6 +16,7 @@ async function saveFile() {
|
|
|
16
16
|
const content = this.lines.join('\n');
|
|
17
17
|
try {
|
|
18
18
|
await fs.writeFile(this.filepath, content, 'utf-8');
|
|
19
|
+
await this.swapManager.clear(); // Clear swap on successful save
|
|
19
20
|
this.isDirty = false; // Reset dirty flag
|
|
20
21
|
this.quitConfirm = false; // Reset quit confirmation
|
|
21
22
|
this.setStatusMessage(`Saved: ${this.filepath}`, 2000);
|
package/dist/editor.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import keypress from './vendor/keypress.js';
|
|
3
3
|
import { ANSI } from './constants.js';
|
|
4
4
|
import { HistoryManager } from './history.js';
|
|
5
|
+
import { SwapManager } from './editor.swap.js';
|
|
5
6
|
// Block `declare module 'keypress'` đã bị xóa
|
|
6
7
|
// Import all functional modules
|
|
7
8
|
import { editingMethods } from './editor.editing.js';
|
|
@@ -18,7 +19,7 @@ const DEFAULT_STATUS = 'HELP: Ctrl+S = Save | Ctrl+Q = Quit | Ctrl+W = Find | Ct
|
|
|
18
19
|
* Main editor class managing application state, TTY interaction, and rendering.
|
|
19
20
|
*/
|
|
20
21
|
export class CliEditor {
|
|
21
|
-
constructor(initialContent, filepath) {
|
|
22
|
+
constructor(initialContent, filepath, options = {}) {
|
|
22
23
|
this.isDirty = false;
|
|
23
24
|
this.cursorX = 0;
|
|
24
25
|
this.cursorY = 0;
|
|
@@ -27,6 +28,7 @@ export class CliEditor {
|
|
|
27
28
|
this.screenRows = 0;
|
|
28
29
|
this.screenCols = 0;
|
|
29
30
|
this.gutterWidth = 5;
|
|
31
|
+
this.tabSize = 4;
|
|
30
32
|
this.screenStartRow = 1;
|
|
31
33
|
this.visualRows = [];
|
|
32
34
|
this.mode = 'edit';
|
|
@@ -39,6 +41,8 @@ export class CliEditor {
|
|
|
39
41
|
this.replaceQuery = null; // null = Find mode, string = Replace mode
|
|
40
42
|
this.goToLineQuery = ''; // For Go to Line prompt
|
|
41
43
|
this.searchResults = [];
|
|
44
|
+
// Map<lineNumber, Array<{ start, end }>> for fast rendering lookup
|
|
45
|
+
this.searchResultMap = new Map();
|
|
42
46
|
this.searchResultIndex = -1;
|
|
43
47
|
this.isCleanedUp = false;
|
|
44
48
|
this.resolvePromise = null;
|
|
@@ -50,29 +54,39 @@ export class CliEditor {
|
|
|
50
54
|
this.lines = [''];
|
|
51
55
|
}
|
|
52
56
|
this.filepath = filepath;
|
|
57
|
+
this.gutterWidth = options.gutterWidth ?? 5;
|
|
58
|
+
this.tabSize = options.tabSize ?? 4;
|
|
59
|
+
this.inputStream = options.inputStream || process.stdin;
|
|
53
60
|
this.history = new HistoryManager();
|
|
54
61
|
this.saveState(true);
|
|
62
|
+
// Initialize SwapManager
|
|
63
|
+
this.swapManager = new SwapManager(this.filepath, () => this.lines.join('\n'));
|
|
55
64
|
}
|
|
56
65
|
// --- Lifecycle Methods ---
|
|
57
66
|
run() {
|
|
58
67
|
this.setupTerminal();
|
|
59
68
|
this.render();
|
|
69
|
+
this.swapManager.start();
|
|
60
70
|
return new Promise((resolve, reject) => {
|
|
61
71
|
const performCleanup = (callback) => {
|
|
72
|
+
this.swapManager.stop(); // Stop swap interval
|
|
62
73
|
if (this.isCleanedUp) {
|
|
63
74
|
if (callback)
|
|
64
75
|
callback();
|
|
65
76
|
return;
|
|
66
77
|
}
|
|
67
78
|
// 1. Remove listeners immediately
|
|
68
|
-
|
|
79
|
+
this.inputStream.removeAllListeners('keypress');
|
|
69
80
|
process.stdout.removeAllListeners('resize');
|
|
70
81
|
// 2. (FIX GHOST TUI) Write exit sequence and use callback to ensure it's written
|
|
71
82
|
// before Node.js fully releases the TTY.
|
|
72
|
-
|
|
83
|
+
// Disable mouse tracking (1000 and 1006)
|
|
84
|
+
process.stdout.write(ANSI.CLEAR_SCREEN + ANSI.MOVE_CURSOR_TOP_LEFT + ANSI.SHOW_CURSOR + ANSI.EXIT_ALTERNATE_SCREEN + '\x1b[?1000l' + '\x1b[?1006l', () => {
|
|
73
85
|
// 3. Disable TTY raw mode and pause stdin after screen is cleared
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
if (this.inputStream.setRawMode) {
|
|
87
|
+
this.inputStream.setRawMode(false);
|
|
88
|
+
}
|
|
89
|
+
this.inputStream.pause();
|
|
76
90
|
this.isCleanedUp = true;
|
|
77
91
|
if (callback)
|
|
78
92
|
callback();
|
|
@@ -87,19 +101,27 @@ export class CliEditor {
|
|
|
87
101
|
});
|
|
88
102
|
}
|
|
89
103
|
setupTerminal() {
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
// If we are using a custom inputStream (re-opened TTY), it might be a ReadStream which is TTY.
|
|
105
|
+
// Check if it is TTY
|
|
106
|
+
if (!this.inputStream.isTTY && !process.stdin.isTTY) {
|
|
107
|
+
// If both are not TTY, we have a problem.
|
|
108
|
+
// But if inputStream is our manually opened TTY, isTTY should be true.
|
|
109
|
+
}
|
|
110
|
+
if (!process.stdout.isTTY) {
|
|
111
|
+
throw new Error('Editor requires a TTY environment (stdout).');
|
|
92
112
|
}
|
|
93
113
|
this.updateScreenSize();
|
|
94
114
|
this.recalculateVisualRows();
|
|
95
|
-
// Enter alternate screen and hide cursor
|
|
96
|
-
process.stdout.write(ANSI.ENTER_ALTERNATE_SCREEN + ANSI.HIDE_CURSOR + ANSI.CLEAR_SCREEN);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
115
|
+
// Enter alternate screen and hide cursor + Enable SGR Mouse (1006) and Button Event (1000)
|
|
116
|
+
process.stdout.write(ANSI.ENTER_ALTERNATE_SCREEN + ANSI.HIDE_CURSOR + ANSI.CLEAR_SCREEN + '\x1b[?1000h' + '\x1b[?1006h');
|
|
117
|
+
if (this.inputStream.setRawMode) {
|
|
118
|
+
this.inputStream.setRawMode(true);
|
|
119
|
+
}
|
|
120
|
+
this.inputStream.resume();
|
|
121
|
+
this.inputStream.setEncoding('utf-8');
|
|
100
122
|
// Setup keypress listener
|
|
101
|
-
keypress(
|
|
102
|
-
|
|
123
|
+
keypress(this.inputStream);
|
|
124
|
+
this.inputStream.on('keypress', this.handleKeypressEvent.bind(this));
|
|
103
125
|
process.stdout.on('resize', this.handleResize.bind(this));
|
|
104
126
|
}
|
|
105
127
|
handleResize() {
|
package/dist/editor.keys.d.ts
CHANGED
|
@@ -10,5 +10,6 @@ export type TKeyHandlingMethods = {
|
|
|
10
10
|
handleCharacterKey: (ch: string) => void;
|
|
11
11
|
cutSelection: () => Promise<void>;
|
|
12
12
|
handleSave: () => Promise<void>;
|
|
13
|
+
handleAltArrows: (keyName: string) => void;
|
|
13
14
|
};
|
|
14
15
|
export declare const keyHandlingMethods: TKeyHandlingMethods;
|
package/dist/editor.keys.js
CHANGED
|
@@ -81,6 +81,15 @@ function handleKeypressEvent(ch, key) {
|
|
|
81
81
|
keyName = KEYS.ENTER;
|
|
82
82
|
else if (key.name === 'tab')
|
|
83
83
|
keyName = 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
|
+
// Handle Mouse Scroll events explicitly
|
|
89
|
+
else if (key.name === 'scrollup')
|
|
90
|
+
keyName = 'SCROLL_UP';
|
|
91
|
+
else if (key.name === 'scrolldown')
|
|
92
|
+
keyName = 'SCROLL_DOWN';
|
|
84
93
|
else
|
|
85
94
|
keyName = key.sequence;
|
|
86
95
|
}
|
|
@@ -114,7 +123,55 @@ function handleKeypressEvent(ch, key) {
|
|
|
114
123
|
return;
|
|
115
124
|
}
|
|
116
125
|
// 5. Xử lý tất cả các phím lệnh/chỉnh sửa khác
|
|
117
|
-
|
|
126
|
+
if (keyName === 'SCROLL_UP') {
|
|
127
|
+
const scrollAmount = 3;
|
|
128
|
+
this.rowOffset = Math.max(0, this.rowOffset - scrollAmount);
|
|
129
|
+
// Adjust cursor if it falls out of view (below the viewport)
|
|
130
|
+
// Actually if we scroll UP, the viewport moves UP. The cursor might be BELOW the viewport.
|
|
131
|
+
// Wait, scroll UP means viewing lines ABOVE. Viewport index decreases.
|
|
132
|
+
// Cursor (if previously in view) might now be >= rowOffset + screenRows.
|
|
133
|
+
// We need to ensure cursor is within [rowOffset, rowOffset + screenRows - 1]
|
|
134
|
+
// But verify after setting rowOffset.
|
|
135
|
+
const currentVisualRow = this.findCurrentVisualRowIndex();
|
|
136
|
+
const bottomEdge = this.rowOffset + this.screenRows - 1;
|
|
137
|
+
if (currentVisualRow > bottomEdge) {
|
|
138
|
+
const targetRow = this.visualRows[bottomEdge];
|
|
139
|
+
this.cursorY = targetRow.logicalY;
|
|
140
|
+
this.cursorX = targetRow.logicalXStart;
|
|
141
|
+
}
|
|
142
|
+
else if (currentVisualRow < this.rowOffset) {
|
|
143
|
+
// Should not happen when scrolling up (moving viewport up), unless cursor was already above?
|
|
144
|
+
// If we scroll up, rowOffset decreases. Current row stays same.
|
|
145
|
+
// So current row > new rowOffset.
|
|
146
|
+
// It might be > bottomEdge.
|
|
147
|
+
}
|
|
148
|
+
// However, to be safe against 'scroll' method resetting it:
|
|
149
|
+
// The 'scroll' method checks:
|
|
150
|
+
// if (currentVisualRow < this.rowOffset) -> this.rowOffset = currentVisualRow
|
|
151
|
+
// if (currentVisualRow >= this.rowOffset + this.screenRows) -> this.rowOffset = ...
|
|
152
|
+
// So we MUST move cursor inside the new viewport.
|
|
153
|
+
if (currentVisualRow > bottomEdge) {
|
|
154
|
+
const targetRow = this.visualRows[bottomEdge];
|
|
155
|
+
this.cursorY = targetRow.logicalY;
|
|
156
|
+
this.cursorX = targetRow.logicalXStart;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else if (keyName === 'SCROLL_DOWN') {
|
|
160
|
+
const scrollAmount = 3;
|
|
161
|
+
const maxOffset = Math.max(0, this.visualRows.length - this.screenRows);
|
|
162
|
+
this.rowOffset = Math.min(maxOffset, this.rowOffset + scrollAmount);
|
|
163
|
+
// Scroll DOWN means viewport index increases.
|
|
164
|
+
// Cursor might be ABOVE the new viewport (currentVisualRow < rowOffset).
|
|
165
|
+
const currentVisualRow = this.findCurrentVisualRowIndex();
|
|
166
|
+
if (currentVisualRow < this.rowOffset) {
|
|
167
|
+
const targetRow = this.visualRows[this.rowOffset];
|
|
168
|
+
this.cursorY = targetRow.logicalY;
|
|
169
|
+
this.cursorX = targetRow.logicalXStart;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
edited = this.handleEditKeys(keyName || ch);
|
|
174
|
+
}
|
|
118
175
|
}
|
|
119
176
|
// 6. Cập nhật Trạng thái và Render
|
|
120
177
|
if (edited) {
|
|
@@ -125,6 +182,13 @@ function handleKeypressEvent(ch, key) {
|
|
|
125
182
|
this.render(); // Render cuối cùng (với visual rows đã được cập nhật nếu cần)
|
|
126
183
|
}
|
|
127
184
|
}
|
|
185
|
+
function handleAltArrows(keyName) {
|
|
186
|
+
this.clearSearchResults(); // Clear highlights on smart navigation
|
|
187
|
+
if (keyName === 'ALT_LEFT')
|
|
188
|
+
this.moveCursorByWord('left');
|
|
189
|
+
else if (keyName === 'ALT_RIGHT')
|
|
190
|
+
this.moveCursorByWord('right');
|
|
191
|
+
}
|
|
128
192
|
/**
|
|
129
193
|
* Handles all command keys in 'edit' mode.
|
|
130
194
|
* Returns true if content was modified.
|
|
@@ -137,6 +201,7 @@ function handleEditKeys(key) {
|
|
|
137
201
|
].includes(key);
|
|
138
202
|
if (isNavigation) {
|
|
139
203
|
this.cancelSelection();
|
|
204
|
+
this.clearSearchResults(); // Clear highlights on navigation
|
|
140
205
|
if (this.isMessageCustom) {
|
|
141
206
|
this.setStatusMessage(this.DEFAULT_STATUS, 0);
|
|
142
207
|
}
|
|
@@ -179,9 +244,11 @@ function handleEditKeys(key) {
|
|
|
179
244
|
return false;
|
|
180
245
|
// --- Editing ---
|
|
181
246
|
case KEYS.ENTER:
|
|
247
|
+
this.clearSearchResults();
|
|
182
248
|
this.insertNewLine();
|
|
183
249
|
return true;
|
|
184
250
|
case KEYS.BACKSPACE:
|
|
251
|
+
this.clearSearchResults();
|
|
185
252
|
// Handle auto-pair deletion
|
|
186
253
|
const line = this.lines[this.cursorY] || '';
|
|
187
254
|
const charBefore = line[this.cursorX - 1];
|
|
@@ -202,12 +269,14 @@ function handleEditKeys(key) {
|
|
|
202
269
|
}
|
|
203
270
|
return true;
|
|
204
271
|
case KEYS.DELETE:
|
|
272
|
+
this.clearSearchResults();
|
|
205
273
|
if (this.selectionAnchor)
|
|
206
274
|
this.deleteSelectedText();
|
|
207
275
|
else
|
|
208
276
|
this.deleteForward();
|
|
209
277
|
return true;
|
|
210
278
|
case KEYS.TAB:
|
|
279
|
+
this.clearSearchResults();
|
|
211
280
|
this.insertSoftTab();
|
|
212
281
|
return true;
|
|
213
282
|
// --- Search & History ---
|
|
@@ -223,6 +292,20 @@ function handleEditKeys(key) {
|
|
|
223
292
|
case KEYS.CTRL_G:
|
|
224
293
|
this.findNext();
|
|
225
294
|
return false;
|
|
295
|
+
// --- Smart Navigation ---
|
|
296
|
+
case 'ALT_LEFT':
|
|
297
|
+
this.moveCursorByWord('left');
|
|
298
|
+
return false;
|
|
299
|
+
case 'ALT_RIGHT':
|
|
300
|
+
this.moveCursorByWord('right');
|
|
301
|
+
return false;
|
|
302
|
+
case KEYS.CTRL_M: // Or any key for Bracket Match. Ctrl+M is technically Enter in some terms but if available...
|
|
303
|
+
// Let's use Ctrl+B (Bracket) if not taken? Ctrl+B is often bold, but here it's CLI.
|
|
304
|
+
// Or just check if key is match bracket key.
|
|
305
|
+
// Let's try to map a specific key or use Meta.
|
|
306
|
+
// For now, let's use Ctrl+B?
|
|
307
|
+
this.matchBracket();
|
|
308
|
+
return false;
|
|
226
309
|
// ***** SỬA LỖI VISUAL *****
|
|
227
310
|
// Sau khi undo/redo, chúng ta PHẢI tính toán lại visual rows
|
|
228
311
|
case KEYS.CTRL_Z:
|
|
@@ -249,6 +332,7 @@ function handleEditKeys(key) {
|
|
|
249
332
|
// Xử lý Ký tự in được
|
|
250
333
|
default:
|
|
251
334
|
if (key.length === 1 && key >= ' ' && key <= '~') {
|
|
335
|
+
this.clearSearchResults();
|
|
252
336
|
this.handleCharacterKey(key);
|
|
253
337
|
return true;
|
|
254
338
|
}
|
|
@@ -465,4 +549,5 @@ export const keyHandlingMethods = {
|
|
|
465
549
|
handleCharacterKey,
|
|
466
550
|
cutSelection,
|
|
467
551
|
handleSave,
|
|
552
|
+
handleAltArrows,
|
|
468
553
|
};
|
|
@@ -48,5 +48,9 @@ export declare const navigationMethods: {
|
|
|
48
48
|
scroll: typeof scroll;
|
|
49
49
|
jumpToLine: typeof jumpToLine;
|
|
50
50
|
enterGoToLineMode: typeof enterGoToLineMode;
|
|
51
|
+
moveCursorByWord: typeof moveCursorByWord;
|
|
52
|
+
matchBracket: typeof matchBracket;
|
|
51
53
|
};
|
|
54
|
+
declare function moveCursorByWord(this: CliEditor, direction: 'left' | 'right'): void;
|
|
55
|
+
declare function matchBracket(this: CliEditor): void;
|
|
52
56
|
export {};
|
|
@@ -153,4 +153,95 @@ export const navigationMethods = {
|
|
|
153
153
|
scroll,
|
|
154
154
|
jumpToLine,
|
|
155
155
|
enterGoToLineMode,
|
|
156
|
+
moveCursorByWord,
|
|
157
|
+
matchBracket,
|
|
156
158
|
};
|
|
159
|
+
function moveCursorByWord(direction) {
|
|
160
|
+
const line = this.lines[this.cursorY];
|
|
161
|
+
if (direction === 'left') {
|
|
162
|
+
if (this.cursorX === 0) {
|
|
163
|
+
if (this.cursorY > 0) {
|
|
164
|
+
this.cursorY--;
|
|
165
|
+
this.cursorX = this.lines[this.cursorY].length;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
// Move left until we hit a non-word char, then until we hit a word char
|
|
170
|
+
// Simple logic: skip whitespace, then skip word chars
|
|
171
|
+
let i = this.cursorX - 1;
|
|
172
|
+
// 1. Skip spaces if we are currently on a space
|
|
173
|
+
while (i > 0 && line[i] === ' ')
|
|
174
|
+
i--;
|
|
175
|
+
// 2. Skip non-spaces
|
|
176
|
+
while (i > 0 && line[i - 1] !== ' ')
|
|
177
|
+
i--;
|
|
178
|
+
this.cursorX = i;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
if (this.cursorX >= line.length) {
|
|
183
|
+
if (this.cursorY < this.lines.length - 1) {
|
|
184
|
+
this.cursorY++;
|
|
185
|
+
this.cursorX = 0;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
let i = this.cursorX;
|
|
190
|
+
// 1. Skip current word chars
|
|
191
|
+
while (i < line.length && line[i] !== ' ')
|
|
192
|
+
i++;
|
|
193
|
+
// 2. Skip spaces
|
|
194
|
+
while (i < line.length && line[i] === ' ')
|
|
195
|
+
i++;
|
|
196
|
+
this.cursorX = i;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function matchBracket() {
|
|
201
|
+
const line = this.lines[this.cursorY];
|
|
202
|
+
const char = line[this.cursorX];
|
|
203
|
+
const pairs = { '(': ')', '[': ']', '{': '}' };
|
|
204
|
+
const revPairs = { ')': '(', ']': '[', '}': '{' };
|
|
205
|
+
if (pairs[char]) {
|
|
206
|
+
// Find closing
|
|
207
|
+
let depth = 1;
|
|
208
|
+
// Search forward
|
|
209
|
+
for (let y = this.cursorY; y < this.lines.length; y++) {
|
|
210
|
+
const l = this.lines[y];
|
|
211
|
+
const startX = (y === this.cursorY) ? this.cursorX + 1 : 0;
|
|
212
|
+
for (let x = startX; x < l.length; x++) {
|
|
213
|
+
if (l[x] === char)
|
|
214
|
+
depth++;
|
|
215
|
+
else if (l[x] === pairs[char])
|
|
216
|
+
depth--;
|
|
217
|
+
if (depth === 0) {
|
|
218
|
+
this.cursorY = y;
|
|
219
|
+
this.cursorX = x;
|
|
220
|
+
this.scroll();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else if (revPairs[char]) {
|
|
227
|
+
// Find opening
|
|
228
|
+
let depth = 1;
|
|
229
|
+
// Search backward
|
|
230
|
+
for (let y = this.cursorY; y >= 0; y--) {
|
|
231
|
+
const l = this.lines[y];
|
|
232
|
+
const startX = (y === this.cursorY) ? this.cursorX - 1 : l.length - 1;
|
|
233
|
+
for (let x = startX; x >= 0; x--) {
|
|
234
|
+
if (l[x] === char)
|
|
235
|
+
depth++;
|
|
236
|
+
else if (l[x] === revPairs[char])
|
|
237
|
+
depth--;
|
|
238
|
+
if (depth === 0) {
|
|
239
|
+
this.cursorY = y;
|
|
240
|
+
this.cursorX = x;
|
|
241
|
+
this.scroll();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
package/dist/editor.rendering.js
CHANGED
|
@@ -41,6 +41,11 @@ function render() {
|
|
|
41
41
|
const displayX = cursorVisualX + this.gutterWidth;
|
|
42
42
|
const displayY = this.screenStartRow + (currentVisualRowIndex - this.rowOffset);
|
|
43
43
|
const selectionRange = this.getNormalizedSelection();
|
|
44
|
+
// Scrollbar calculations
|
|
45
|
+
const totalLines = this.visualRows.length;
|
|
46
|
+
const showScrollbar = totalLines > this.screenRows;
|
|
47
|
+
const thumbHeight = showScrollbar ? Math.max(1, Math.floor((this.screenRows / totalLines) * this.screenRows)) : 0;
|
|
48
|
+
const thumbStart = showScrollbar ? Math.floor((this.rowOffset / totalLines) * this.screenRows) : 0;
|
|
44
49
|
// Draw visual rows
|
|
45
50
|
for (let y = 0; y < this.screenRows; y++) {
|
|
46
51
|
const visualRowIndex = y + this.rowOffset;
|
|
@@ -66,7 +71,19 @@ function render() {
|
|
|
66
71
|
const isCursorPosition = (visualRowIndex === currentVisualRowIndex && i === cursorVisualX);
|
|
67
72
|
const isSelected = selectionRange && this.isPositionInSelection(logicalY, logicalX, selectionRange);
|
|
68
73
|
// Highlight search result under cursor
|
|
69
|
-
|
|
74
|
+
// Check if this character is part of ANY search result
|
|
75
|
+
let isGlobalSearchResult = false;
|
|
76
|
+
if (this.searchResultMap.has(logicalY)) {
|
|
77
|
+
const matches = this.searchResultMap.get(logicalY);
|
|
78
|
+
for (const match of matches) {
|
|
79
|
+
if (logicalX >= match.start && logicalX < match.end) {
|
|
80
|
+
isGlobalSearchResult = true;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Check if this character is part of the CURRENTLY SELECTED search result
|
|
86
|
+
const isCurrentSearchResult = (this.searchResultIndex !== -1 &&
|
|
70
87
|
this.searchResults[this.searchResultIndex]?.y === logicalY &&
|
|
71
88
|
logicalX >= this.searchResults[this.searchResultIndex]?.x &&
|
|
72
89
|
logicalX < (this.searchResults[this.searchResultIndex]?.x + this.searchQuery.length));
|
|
@@ -77,8 +94,12 @@ function render() {
|
|
|
77
94
|
// Cursor is a single inverted character if not already covered by selection
|
|
78
95
|
buffer += ANSI.INVERT_COLORS + char + ANSI.RESET_COLORS;
|
|
79
96
|
}
|
|
80
|
-
else if (
|
|
81
|
-
//
|
|
97
|
+
else if (isCurrentSearchResult) {
|
|
98
|
+
// Selected Match: Invert + Underline (if supported) or just Invert
|
|
99
|
+
buffer += ANSI.INVERT_COLORS + '\x1b[4m' + char + ANSI.RESET_COLORS;
|
|
100
|
+
}
|
|
101
|
+
else if (isGlobalSearchResult) {
|
|
102
|
+
// Global Match: Invert only
|
|
82
103
|
buffer += ANSI.INVERT_COLORS + char + ANSI.RESET_COLORS;
|
|
83
104
|
}
|
|
84
105
|
else {
|
|
@@ -93,6 +114,13 @@ function render() {
|
|
|
93
114
|
}
|
|
94
115
|
buffer += `${ANSI.CLEAR_LINE}`;
|
|
95
116
|
}
|
|
117
|
+
// Draw Scrollbar (Phase 2)
|
|
118
|
+
if (showScrollbar) {
|
|
119
|
+
const isThumb = y >= thumbStart && y < thumbStart + thumbHeight;
|
|
120
|
+
const scrollChar = isThumb ? '┃' : '│';
|
|
121
|
+
// Move to last column and draw
|
|
122
|
+
buffer += `\x1b[${this.screenStartRow + y};${this.screenCols}H${ANSI.RESET_COLORS}${scrollChar}`;
|
|
123
|
+
}
|
|
96
124
|
}
|
|
97
125
|
// Draw status bar
|
|
98
126
|
buffer += `\x1b[${this.screenRows + this.screenStartRow};1H`;
|
package/dist/editor.search.d.ts
CHANGED
|
@@ -33,6 +33,10 @@ declare function jumpToResult(this: CliEditor, result: {
|
|
|
33
33
|
y: number;
|
|
34
34
|
x: number;
|
|
35
35
|
}): void;
|
|
36
|
+
/**
|
|
37
|
+
* Clears the current search results and highlights.
|
|
38
|
+
*/
|
|
39
|
+
declare function clearSearchResults(this: CliEditor): void;
|
|
36
40
|
export declare const searchMethods: {
|
|
37
41
|
enterFindMode: typeof enterFindMode;
|
|
38
42
|
enterReplaceMode: typeof enterReplaceMode;
|
|
@@ -41,5 +45,6 @@ export declare const searchMethods: {
|
|
|
41
45
|
replaceCurrentAndFindNext: typeof replaceCurrentAndFindNext;
|
|
42
46
|
replaceAll: typeof replaceAll;
|
|
43
47
|
jumpToResult: typeof jumpToResult;
|
|
48
|
+
clearSearchResults: typeof clearSearchResults;
|
|
44
49
|
};
|
|
45
50
|
export {};
|
package/dist/editor.search.js
CHANGED
|
@@ -29,13 +29,20 @@ function enterReplaceMode() {
|
|
|
29
29
|
*/
|
|
30
30
|
function executeSearch() {
|
|
31
31
|
this.searchResults = [];
|
|
32
|
+
this.searchResultMap.clear();
|
|
32
33
|
if (this.searchQuery === '')
|
|
33
34
|
return;
|
|
35
|
+
const queryLen = this.searchQuery.length;
|
|
34
36
|
for (let y = 0; y < this.lines.length; y++) {
|
|
35
37
|
const line = this.lines[y];
|
|
36
38
|
let index = -1;
|
|
39
|
+
const lineMatches = [];
|
|
37
40
|
while ((index = line.indexOf(this.searchQuery, index + 1)) !== -1) {
|
|
38
41
|
this.searchResults.push({ y, x: index });
|
|
42
|
+
lineMatches.push({ start: index, end: index + queryLen });
|
|
43
|
+
}
|
|
44
|
+
if (lineMatches.length > 0) {
|
|
45
|
+
this.searchResultMap.set(y, lineMatches);
|
|
39
46
|
}
|
|
40
47
|
}
|
|
41
48
|
this.searchResultIndex = -1;
|
|
@@ -158,6 +165,14 @@ function jumpToResult(result) {
|
|
|
158
165
|
// Calculate new scroll offset to center the result visually
|
|
159
166
|
this.rowOffset = Math.max(0, visualRowIndex - Math.floor(this.screenRows / 2));
|
|
160
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Clears the current search results and highlights.
|
|
170
|
+
*/
|
|
171
|
+
function clearSearchResults() {
|
|
172
|
+
this.searchResults = [];
|
|
173
|
+
this.searchResultMap.clear();
|
|
174
|
+
this.searchResultIndex = -1;
|
|
175
|
+
}
|
|
161
176
|
export const searchMethods = {
|
|
162
177
|
enterFindMode,
|
|
163
178
|
enterReplaceMode,
|
|
@@ -166,4 +181,5 @@ export const searchMethods = {
|
|
|
166
181
|
replaceCurrentAndFindNext,
|
|
167
182
|
replaceAll,
|
|
168
183
|
jumpToResult,
|
|
184
|
+
clearSearchResults,
|
|
169
185
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare class SwapManager {
|
|
2
|
+
private filepath;
|
|
3
|
+
private swapPath;
|
|
4
|
+
private intervalId;
|
|
5
|
+
private contentGetter;
|
|
6
|
+
private lastSavedContent;
|
|
7
|
+
private intervalMs;
|
|
8
|
+
constructor(filepath: string, contentGetter: () => string);
|
|
9
|
+
start(): void;
|
|
10
|
+
stop(): void;
|
|
11
|
+
update(): Promise<void>;
|
|
12
|
+
private saveSwap;
|
|
13
|
+
clear(): Promise<void>;
|
|
14
|
+
static getSwapPath(filepath: string): string;
|
|
15
|
+
static check(filepath: string): Promise<boolean>;
|
|
16
|
+
static read(filepath: string): Promise<string>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// src/editor.swap.ts
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
export class SwapManager {
|
|
5
|
+
constructor(filepath, contentGetter) {
|
|
6
|
+
this.intervalId = null;
|
|
7
|
+
this.lastSavedContent = '';
|
|
8
|
+
this.intervalMs = 2000; // 2 seconds
|
|
9
|
+
this.filepath = filepath;
|
|
10
|
+
// If filepath is empty (e.g. untitled/piped), we can't really make a relative swap file easily.
|
|
11
|
+
// We will default to a temp file or current dir if filepath is empty.
|
|
12
|
+
// For now, let's assume if filepath is provided, we use it.
|
|
13
|
+
if (!filepath) {
|
|
14
|
+
this.swapPath = path.resolve('.untitled.swp');
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
const dir = path.dirname(filepath);
|
|
18
|
+
const file = path.basename(filepath);
|
|
19
|
+
this.swapPath = path.join(dir, '.' + file + '.swp');
|
|
20
|
+
}
|
|
21
|
+
this.contentGetter = contentGetter;
|
|
22
|
+
}
|
|
23
|
+
start() {
|
|
24
|
+
if (this.intervalId)
|
|
25
|
+
return;
|
|
26
|
+
this.intervalId = setInterval(() => this.saveSwap(), this.intervalMs);
|
|
27
|
+
}
|
|
28
|
+
stop() {
|
|
29
|
+
if (this.intervalId) {
|
|
30
|
+
clearInterval(this.intervalId);
|
|
31
|
+
this.intervalId = null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Explicitly update swap (can be called on keypress if we want instant-ish updates)
|
|
35
|
+
async update() {
|
|
36
|
+
await this.saveSwap();
|
|
37
|
+
}
|
|
38
|
+
async saveSwap() {
|
|
39
|
+
const currentContent = this.contentGetter();
|
|
40
|
+
// Optimization: Don't write if nothing changed since last swap save
|
|
41
|
+
if (currentContent === this.lastSavedContent)
|
|
42
|
+
return;
|
|
43
|
+
try {
|
|
44
|
+
await fs.writeFile(this.swapPath, currentContent, 'utf-8');
|
|
45
|
+
this.lastSavedContent = currentContent;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
// Silently ignore swap errors to not disrupt user flow
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async clear() {
|
|
52
|
+
this.stop();
|
|
53
|
+
try {
|
|
54
|
+
await fs.unlink(this.swapPath);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
if (err.code !== 'ENOENT') {
|
|
58
|
+
// ignore
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
static getSwapPath(filepath) {
|
|
63
|
+
if (!filepath)
|
|
64
|
+
return path.resolve('.untitled.swp');
|
|
65
|
+
const dir = path.dirname(filepath);
|
|
66
|
+
const file = path.basename(filepath);
|
|
67
|
+
return path.join(dir, '.' + file + '.swp');
|
|
68
|
+
}
|
|
69
|
+
static async check(filepath) {
|
|
70
|
+
const swapPath = SwapManager.getSwapPath(filepath);
|
|
71
|
+
try {
|
|
72
|
+
await fs.access(swapPath);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
static async read(filepath) {
|
|
80
|
+
const swapPath = SwapManager.getSwapPath(filepath);
|
|
81
|
+
return fs.readFile(swapPath, 'utf-8');
|
|
82
|
+
}
|
|
83
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { EditorOptions } from './types.js';
|
|
1
2
|
/**
|
|
2
3
|
* Public API function: Opens the editor.
|
|
3
4
|
* Reads the file and initializes CliEditor.
|
|
4
5
|
*/
|
|
5
|
-
export declare function openEditor(filepath: string): Promise<{
|
|
6
|
+
export declare function openEditor(filepath: string, options?: EditorOptions): Promise<{
|
|
6
7
|
saved: boolean;
|
|
7
8
|
content: string;
|
|
8
9
|
}>;
|
package/dist/index.js
CHANGED
|
@@ -1,24 +1,62 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { promises as fs } from 'fs';
|
|
3
3
|
import { CliEditor } from './editor.js';
|
|
4
|
+
import { SwapManager } from './editor.swap.js';
|
|
4
5
|
/**
|
|
5
6
|
* Public API function: Opens the editor.
|
|
6
7
|
* Reads the file and initializes CliEditor.
|
|
7
8
|
*/
|
|
8
|
-
export async function openEditor(filepath) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
export async function openEditor(filepath, options) {
|
|
10
|
+
// 0. Handle Piping (Stdin)
|
|
11
|
+
let pipedContent = '';
|
|
12
|
+
if (!process.stdin.isTTY) {
|
|
13
|
+
try {
|
|
14
|
+
const chunks = [];
|
|
15
|
+
for await (const chunk of process.stdin) {
|
|
16
|
+
chunks.push(chunk);
|
|
17
|
+
}
|
|
18
|
+
pipedContent = Buffer.concat(chunks).toString('utf-8');
|
|
19
|
+
// CRITICAL: Re-open TTY for user input!
|
|
20
|
+
// We need to bypass the consumed stdin and open the actual terminal device.
|
|
21
|
+
const ttyPath = process.platform === 'win32' ? 'CONIN$' : '/dev/tty';
|
|
22
|
+
const ttyFd = await fs.open(ttyPath, 'r');
|
|
23
|
+
// Let's rely on the fact that we can construct a new ReadStream.
|
|
24
|
+
const { ReadStream } = await import('tty');
|
|
25
|
+
const ttyReadStream = new ReadStream(ttyFd.fd);
|
|
26
|
+
if (options) {
|
|
27
|
+
options.inputStream = ttyReadStream;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
options = { inputStream: ttyReadStream };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
console.error('Failed to read from stdin or open TTY:', e);
|
|
35
|
+
}
|
|
13
36
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
37
|
+
// Check for swap file (only if filepath provided)
|
|
38
|
+
if (filepath && await SwapManager.check(filepath)) {
|
|
39
|
+
console.log(`\x1b[33mWarning: Swap file detected for ${filepath}. Recovering content...\x1b[0m`);
|
|
40
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
41
|
+
const swapContent = await SwapManager.read(filepath);
|
|
42
|
+
const editor = new CliEditor(swapContent, filepath, options);
|
|
43
|
+
editor.isDirty = true; // Mark as dirty manually to avoid potential mixin issues
|
|
44
|
+
editor.statusMessage = 'RECOVERED FROM SWAP FILE';
|
|
45
|
+
return editor.run();
|
|
46
|
+
}
|
|
47
|
+
let initialContent = pipedContent; // Default to piped content
|
|
48
|
+
if (filepath && !initialContent) {
|
|
49
|
+
try {
|
|
50
|
+
initialContent = await fs.readFile(filepath, 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (err.code !== 'ENOENT') {
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
18
56
|
}
|
|
19
57
|
}
|
|
20
58
|
// 3. Initialize and run editor
|
|
21
|
-
const editor = new CliEditor(initialContent, filepath);
|
|
59
|
+
const editor = new CliEditor(initialContent, filepath, options);
|
|
22
60
|
return editor.run();
|
|
23
61
|
}
|
|
24
62
|
// --- Public Exports ---
|
package/dist/types.d.ts
CHANGED
|
@@ -16,3 +16,8 @@ export interface VisualRow {
|
|
|
16
16
|
content: string;
|
|
17
17
|
}
|
|
18
18
|
export type EditorMode = 'edit' | 'search_find' | 'search_replace' | 'search_confirm' | 'goto_line';
|
|
19
|
+
export interface EditorOptions {
|
|
20
|
+
tabSize?: number;
|
|
21
|
+
gutterWidth?: number;
|
|
22
|
+
inputStream?: any;
|
|
23
|
+
}
|
package/dist/vendor/keypress.js
CHANGED
|
@@ -18,6 +18,7 @@ if (!listenerCount) {
|
|
|
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
|
+
const mouseSgrRe = /^\x1b\[<(\d+);(\d+);(\d+)([mM])/;
|
|
21
22
|
/**
|
|
22
23
|
* Hàm chính, chấp nhận một Readable Stream và làm cho nó
|
|
23
24
|
* phát ra sự kiện "keypress".
|
|
@@ -421,7 +422,32 @@ function emitKey(stream, s) {
|
|
|
421
422
|
}
|
|
422
423
|
return;
|
|
423
424
|
}
|
|
424
|
-
//
|
|
425
|
+
// Mouse handling (SGR 1006)
|
|
426
|
+
if ((parts = mouseSgrRe.exec(s))) {
|
|
427
|
+
// SGR Mode: \x1b[< b; x; y M/m
|
|
428
|
+
// b: button code
|
|
429
|
+
// x, y: coordinates (1-based)
|
|
430
|
+
// M/m: Press/Release
|
|
431
|
+
const b = parseInt(parts[1], 10);
|
|
432
|
+
const x = parseInt(parts[2], 10);
|
|
433
|
+
const y = parseInt(parts[3], 10);
|
|
434
|
+
const type = parts[4]; // M=press, m=release
|
|
435
|
+
key.name = 'mouse';
|
|
436
|
+
key.ctrl = false;
|
|
437
|
+
key.meta = false;
|
|
438
|
+
key.shift = false;
|
|
439
|
+
// Check for Scroll (Button 64 = Up, 65 = Down)
|
|
440
|
+
if (b === 64) {
|
|
441
|
+
key.name = 'scrollup';
|
|
442
|
+
key.code = 'scrollup';
|
|
443
|
+
}
|
|
444
|
+
else if (b === 65) {
|
|
445
|
+
key.name = 'scrolldown';
|
|
446
|
+
key.code = 'scrolldown';
|
|
447
|
+
}
|
|
448
|
+
// We can handle click here if needed (b=0 left, b=1 middle, b=2 right)
|
|
449
|
+
// but for now only scroll is requested.
|
|
450
|
+
}
|
|
425
451
|
// Không phát ra key nếu không tìm thấy tên
|
|
426
452
|
if (key.name === undefined) {
|
|
427
453
|
return; // key = undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cliedit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "tsc",
|
|
23
23
|
"prepublishOnly": "npm run build",
|
|
24
|
-
"demo": "npm run build && cross-env NODE_ENV=development
|
|
24
|
+
"demo": "npm run build && cross-env NODE_ENV=development tsx src/demo.ts",
|
|
25
25
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
26
26
|
},
|
|
27
27
|
"keywords": [
|
|
@@ -35,10 +35,9 @@
|
|
|
35
35
|
"author": "CodeTease",
|
|
36
36
|
"license": "MIT",
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@types/node": "^
|
|
39
|
-
"cross-env": "^7
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
"dependencies": {
|
|
38
|
+
"@types/node": "^22",
|
|
39
|
+
"cross-env": "^7",
|
|
40
|
+
"tsx": "^4",
|
|
41
|
+
"typescript": "^5"
|
|
43
42
|
}
|
|
44
|
-
}
|
|
43
|
+
}
|