cliedit 0.2.0 → 0.3.5
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 +6 -5
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +4 -0
- package/dist/editor.d.ts +8 -1
- package/dist/editor.editing.js +6 -0
- package/dist/editor.history.js +2 -0
- package/dist/editor.js +9 -3
- package/dist/editor.keys.js +61 -1
- package/dist/editor.rendering.js +40 -3
- package/dist/editor.search.d.ts +5 -0
- package/dist/editor.search.js +16 -0
- package/dist/editor.syntax.d.ts +16 -0
- package/dist/editor.syntax.js +84 -0
- package/dist/vendor/keypress.d.ts +4 -4
- package/dist/vendor/keypress.js +58 -32
- package/package.json +11 -9
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ It includes line wrapping, visual navigation, smart auto-indentation, undo/redo,
|
|
|
14
14
|
- **Undo/Redo:** `Ctrl+Z` / `Ctrl+Y` for persistent history.
|
|
15
15
|
- **Text Selection:** `Ctrl+Arrow` keys to select text.
|
|
16
16
|
- **Clipboard Support:** `Ctrl+C` (Copy), `Ctrl+X` (Cut), `Ctrl+V` (Paste) for system clipboard (macOS, Windows, **and Linux** via `xclip`).
|
|
17
|
+
- **Syntax Highlighting:** Lightweight highlighting for Brackets `()` `[]` `{}` and Strings `""` `''`.
|
|
17
18
|
- **File I/O:** Loads from and saves to the filesystem.
|
|
18
19
|
- **Search & Replace:** `Ctrl+W` to find text, `Ctrl+R` to find and replace interactively.
|
|
19
20
|
- **Go to Line:** `Ctrl+L` to quickly jump to a specific line number.
|
|
@@ -23,7 +24,7 @@ It includes line wrapping, visual navigation, smart auto-indentation, undo/redo,
|
|
|
23
24
|
- **Crash Recovery:** Automatically saves changes to a hidden swap file (e.g. `.filename.swp`) to prevent data loss.
|
|
24
25
|
|
|
25
26
|
## Installation
|
|
26
|
-
```
|
|
27
|
+
```shell
|
|
27
28
|
npm install cliedit
|
|
28
29
|
````
|
|
29
30
|
|
|
@@ -31,7 +32,7 @@ npm install cliedit
|
|
|
31
32
|
|
|
32
33
|
The package exports an `async` function `openEditor` that returns a `Promise`. The promise resolves when the user quits the editor.
|
|
33
34
|
|
|
34
|
-
```
|
|
35
|
+
```typescript
|
|
35
36
|
import { openEditor } from 'cliedit';
|
|
36
37
|
import path from 'path';
|
|
37
38
|
|
|
@@ -64,7 +65,7 @@ getCommitMessage();
|
|
|
64
65
|
|
|
65
66
|
`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
|
|
|
67
|
-
```
|
|
68
|
+
```shell
|
|
68
69
|
# Edit a file using cat
|
|
69
70
|
cat README.md | node my-app.js
|
|
70
71
|
|
|
@@ -101,7 +102,7 @@ If the process crashes or is terminated abruptly, the next time you open the fil
|
|
|
101
102
|
|
|
102
103
|
The main editor class. You can import this directly if you need to extend or instantiate the editor with custom logic.
|
|
103
104
|
|
|
104
|
-
```
|
|
105
|
+
```typescript
|
|
105
106
|
import { CliEditor } from 'cliedit';
|
|
106
107
|
```
|
|
107
108
|
|
|
@@ -109,7 +110,7 @@ import { CliEditor } from 'cliedit';
|
|
|
109
110
|
|
|
110
111
|
Key types are also exported for convenience:
|
|
111
112
|
|
|
112
|
-
```
|
|
113
|
+
```typescript
|
|
113
114
|
import type {
|
|
114
115
|
DocumentState,
|
|
115
116
|
VisualRow,
|
package/dist/constants.d.ts
CHANGED
|
@@ -11,6 +11,9 @@ export declare const ANSI: {
|
|
|
11
11
|
RESET_COLORS: string;
|
|
12
12
|
ENTER_ALTERNATE_SCREEN: string;
|
|
13
13
|
EXIT_ALTERNATE_SCREEN: string;
|
|
14
|
+
YELLOW: string;
|
|
15
|
+
CYAN: string;
|
|
16
|
+
GREEN: string;
|
|
14
17
|
};
|
|
15
18
|
/**
|
|
16
19
|
* Key definitions for special keypresses (using Ctrl+ keys for reliable detection).
|
package/dist/constants.js
CHANGED
|
@@ -12,6 +12,10 @@ export const ANSI = {
|
|
|
12
12
|
RESET_COLORS: '\x1b[0m', // Reset colors
|
|
13
13
|
ENTER_ALTERNATE_SCREEN: '\x1b[?1049h', // Enter alternate screen
|
|
14
14
|
EXIT_ALTERNATE_SCREEN: '\x1b[?1049l', // Exit alternate screen
|
|
15
|
+
// Syntax Highlighting Colors
|
|
16
|
+
YELLOW: '\x1b[33m',
|
|
17
|
+
CYAN: '\x1b[36m',
|
|
18
|
+
GREEN: '\x1b[32m',
|
|
15
19
|
};
|
|
16
20
|
/**
|
|
17
21
|
* Key definitions for special keypresses (using Ctrl+ keys for reliable detection).
|
package/dist/editor.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { historyMethods } from './editor.history.js';
|
|
|
10
10
|
import { ioMethods } from './editor.io.js';
|
|
11
11
|
import { TKeyHandlingMethods } from './editor.keys.js';
|
|
12
12
|
import { TSelectionMethods } from './editor.selection.js';
|
|
13
|
+
import { syntaxMethods } from './editor.syntax.js';
|
|
13
14
|
type TEditingMethods = typeof editingMethods;
|
|
14
15
|
type TClipboardMethods = typeof clipboardMethods;
|
|
15
16
|
type TNavigationMethods = typeof navigationMethods;
|
|
@@ -17,7 +18,8 @@ type TRenderingMethods = typeof renderingMethods;
|
|
|
17
18
|
type TSearchMethods = typeof searchMethods;
|
|
18
19
|
type THistoryMethods = typeof historyMethods;
|
|
19
20
|
type TIOMethods = typeof ioMethods;
|
|
20
|
-
|
|
21
|
+
type TSyntaxMethods = typeof syntaxMethods;
|
|
22
|
+
export interface CliEditor extends TEditingMethods, TClipboardMethods, TNavigationMethods, TRenderingMethods, TSearchMethods, THistoryMethods, TIOMethods, TKeyHandlingMethods, TSelectionMethods, TSyntaxMethods {
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
25
|
* Main editor class managing application state, TTY interaction, and rendering.
|
|
@@ -52,7 +54,12 @@ export declare class CliEditor {
|
|
|
52
54
|
y: number;
|
|
53
55
|
x: number;
|
|
54
56
|
}[];
|
|
57
|
+
searchResultMap: Map<number, Array<{
|
|
58
|
+
start: number;
|
|
59
|
+
end: number;
|
|
60
|
+
}>>;
|
|
55
61
|
searchResultIndex: number;
|
|
62
|
+
syntaxCache: Map<number, Map<number, string>>;
|
|
56
63
|
history: HistoryManager;
|
|
57
64
|
swapManager: SwapManager;
|
|
58
65
|
isCleanedUp: boolean;
|
package/dist/editor.editing.js
CHANGED
|
@@ -33,6 +33,7 @@ function insertContentAtCursor(contentLines) {
|
|
|
33
33
|
this.cursorX = lastPasteLine.length;
|
|
34
34
|
}
|
|
35
35
|
this.setDirty();
|
|
36
|
+
this.invalidateSyntaxCache();
|
|
36
37
|
this.recalculateVisualRows();
|
|
37
38
|
}
|
|
38
39
|
/**
|
|
@@ -42,6 +43,7 @@ function insertCharacter(char) {
|
|
|
42
43
|
const line = this.lines[this.cursorY] || '';
|
|
43
44
|
this.lines[this.cursorY] = line.slice(0, this.cursorX) + char + line.slice(this.cursorX);
|
|
44
45
|
this.cursorX += char.length;
|
|
46
|
+
this.invalidateSyntaxCache();
|
|
45
47
|
}
|
|
46
48
|
/**
|
|
47
49
|
* Inserts a soft tab (using configured tabSize).
|
|
@@ -49,6 +51,7 @@ function insertCharacter(char) {
|
|
|
49
51
|
function insertSoftTab() {
|
|
50
52
|
const spaces = ' '.repeat(this.tabSize || 4);
|
|
51
53
|
this.insertCharacter(spaces);
|
|
54
|
+
// invalidation handled in insertCharacter
|
|
52
55
|
}
|
|
53
56
|
/**
|
|
54
57
|
* Inserts a new line, splitting the current line at the cursor position.
|
|
@@ -66,6 +69,7 @@ function insertNewLine() {
|
|
|
66
69
|
this.cursorY++;
|
|
67
70
|
this.cursorX = indent.length; // Move cursor to end of indent
|
|
68
71
|
this.setDirty();
|
|
72
|
+
this.invalidateSyntaxCache();
|
|
69
73
|
}
|
|
70
74
|
/**
|
|
71
75
|
* Deletes the character before the cursor, or joins the current line with the previous one.
|
|
@@ -90,6 +94,7 @@ function deleteBackward() {
|
|
|
90
94
|
this.cursorX = 0;
|
|
91
95
|
}
|
|
92
96
|
this.setDirty();
|
|
97
|
+
this.invalidateSyntaxCache();
|
|
93
98
|
}
|
|
94
99
|
/**
|
|
95
100
|
* Deletes the character after the cursor, or joins the current line with the next one.
|
|
@@ -110,6 +115,7 @@ function deleteForward() {
|
|
|
110
115
|
this.cursorX = 0;
|
|
111
116
|
}
|
|
112
117
|
this.setDirty();
|
|
118
|
+
this.invalidateSyntaxCache();
|
|
113
119
|
}
|
|
114
120
|
/**
|
|
115
121
|
* Handles auto-pairing of brackets and quotes.
|
package/dist/editor.history.js
CHANGED
|
@@ -38,6 +38,7 @@ function undo() {
|
|
|
38
38
|
if (state) {
|
|
39
39
|
this.loadState(state);
|
|
40
40
|
this.setDirty();
|
|
41
|
+
this.invalidateSyntaxCache();
|
|
41
42
|
this.setStatusMessage('Undo successful');
|
|
42
43
|
}
|
|
43
44
|
else {
|
|
@@ -52,6 +53,7 @@ function redo() {
|
|
|
52
53
|
if (state) {
|
|
53
54
|
this.loadState(state);
|
|
54
55
|
this.setDirty();
|
|
56
|
+
this.invalidateSyntaxCache();
|
|
55
57
|
this.setStatusMessage('Redo successful');
|
|
56
58
|
}
|
|
57
59
|
else {
|
package/dist/editor.js
CHANGED
|
@@ -14,6 +14,7 @@ import { historyMethods } from './editor.history.js';
|
|
|
14
14
|
import { ioMethods } from './editor.io.js';
|
|
15
15
|
import { keyHandlingMethods } from './editor.keys.js';
|
|
16
16
|
import { selectionMethods } from './editor.selection.js';
|
|
17
|
+
import { syntaxMethods } from './editor.syntax.js';
|
|
17
18
|
const DEFAULT_STATUS = 'HELP: Ctrl+S = Save | Ctrl+Q = Quit | Ctrl+W = Find | Ctrl+R = Replace | Ctrl+L = Go to Line';
|
|
18
19
|
/**
|
|
19
20
|
* Main editor class managing application state, TTY interaction, and rendering.
|
|
@@ -41,7 +42,10 @@ export class CliEditor {
|
|
|
41
42
|
this.replaceQuery = null; // null = Find mode, string = Replace mode
|
|
42
43
|
this.goToLineQuery = ''; // For Go to Line prompt
|
|
43
44
|
this.searchResults = [];
|
|
45
|
+
// Map<lineNumber, Array<{ start, end }>> for fast rendering lookup
|
|
46
|
+
this.searchResultMap = new Map();
|
|
44
47
|
this.searchResultIndex = -1;
|
|
48
|
+
this.syntaxCache = new Map();
|
|
45
49
|
this.isCleanedUp = false;
|
|
46
50
|
this.resolvePromise = null;
|
|
47
51
|
this.rejectPromise = null;
|
|
@@ -78,7 +82,8 @@ export class CliEditor {
|
|
|
78
82
|
process.stdout.removeAllListeners('resize');
|
|
79
83
|
// 2. (FIX GHOST TUI) Write exit sequence and use callback to ensure it's written
|
|
80
84
|
// before Node.js fully releases the TTY.
|
|
81
|
-
|
|
85
|
+
// Disable mouse tracking (1000 and 1006)
|
|
86
|
+
process.stdout.write(ANSI.CLEAR_SCREEN + ANSI.MOVE_CURSOR_TOP_LEFT + ANSI.SHOW_CURSOR + ANSI.EXIT_ALTERNATE_SCREEN + '\x1b[?1000l' + '\x1b[?1006l', () => {
|
|
82
87
|
// 3. Disable TTY raw mode and pause stdin after screen is cleared
|
|
83
88
|
if (this.inputStream.setRawMode) {
|
|
84
89
|
this.inputStream.setRawMode(false);
|
|
@@ -109,8 +114,8 @@ export class CliEditor {
|
|
|
109
114
|
}
|
|
110
115
|
this.updateScreenSize();
|
|
111
116
|
this.recalculateVisualRows();
|
|
112
|
-
// Enter alternate screen and hide cursor
|
|
113
|
-
process.stdout.write(ANSI.ENTER_ALTERNATE_SCREEN + ANSI.HIDE_CURSOR + ANSI.CLEAR_SCREEN);
|
|
117
|
+
// Enter alternate screen and hide cursor + Enable SGR Mouse (1006) and Button Event (1000)
|
|
118
|
+
process.stdout.write(ANSI.ENTER_ALTERNATE_SCREEN + ANSI.HIDE_CURSOR + ANSI.CLEAR_SCREEN + '\x1b[?1000h' + '\x1b[?1006h');
|
|
114
119
|
if (this.inputStream.setRawMode) {
|
|
115
120
|
this.inputStream.setRawMode(true);
|
|
116
121
|
}
|
|
@@ -142,3 +147,4 @@ Object.assign(CliEditor.prototype, historyMethods);
|
|
|
142
147
|
Object.assign(CliEditor.prototype, ioMethods);
|
|
143
148
|
Object.assign(CliEditor.prototype, keyHandlingMethods);
|
|
144
149
|
Object.assign(CliEditor.prototype, selectionMethods);
|
|
150
|
+
Object.assign(CliEditor.prototype, syntaxMethods);
|
package/dist/editor.keys.js
CHANGED
|
@@ -85,6 +85,11 @@ function handleKeypressEvent(ch, key) {
|
|
|
85
85
|
keyName = 'ALT_LEFT';
|
|
86
86
|
else if (key.meta && key.name === 'right')
|
|
87
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';
|
|
88
93
|
else
|
|
89
94
|
keyName = key.sequence;
|
|
90
95
|
}
|
|
@@ -118,7 +123,55 @@ function handleKeypressEvent(ch, key) {
|
|
|
118
123
|
return;
|
|
119
124
|
}
|
|
120
125
|
// 5. Xử lý tất cả các phím lệnh/chỉnh sửa khác
|
|
121
|
-
|
|
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
|
+
}
|
|
122
175
|
}
|
|
123
176
|
// 6. Cập nhật Trạng thái và Render
|
|
124
177
|
if (edited) {
|
|
@@ -130,6 +183,7 @@ function handleKeypressEvent(ch, key) {
|
|
|
130
183
|
}
|
|
131
184
|
}
|
|
132
185
|
function handleAltArrows(keyName) {
|
|
186
|
+
this.clearSearchResults(); // Clear highlights on smart navigation
|
|
133
187
|
if (keyName === 'ALT_LEFT')
|
|
134
188
|
this.moveCursorByWord('left');
|
|
135
189
|
else if (keyName === 'ALT_RIGHT')
|
|
@@ -147,6 +201,7 @@ function handleEditKeys(key) {
|
|
|
147
201
|
].includes(key);
|
|
148
202
|
if (isNavigation) {
|
|
149
203
|
this.cancelSelection();
|
|
204
|
+
this.clearSearchResults(); // Clear highlights on navigation
|
|
150
205
|
if (this.isMessageCustom) {
|
|
151
206
|
this.setStatusMessage(this.DEFAULT_STATUS, 0);
|
|
152
207
|
}
|
|
@@ -189,9 +244,11 @@ function handleEditKeys(key) {
|
|
|
189
244
|
return false;
|
|
190
245
|
// --- Editing ---
|
|
191
246
|
case KEYS.ENTER:
|
|
247
|
+
this.clearSearchResults();
|
|
192
248
|
this.insertNewLine();
|
|
193
249
|
return true;
|
|
194
250
|
case KEYS.BACKSPACE:
|
|
251
|
+
this.clearSearchResults();
|
|
195
252
|
// Handle auto-pair deletion
|
|
196
253
|
const line = this.lines[this.cursorY] || '';
|
|
197
254
|
const charBefore = line[this.cursorX - 1];
|
|
@@ -212,12 +269,14 @@ function handleEditKeys(key) {
|
|
|
212
269
|
}
|
|
213
270
|
return true;
|
|
214
271
|
case KEYS.DELETE:
|
|
272
|
+
this.clearSearchResults();
|
|
215
273
|
if (this.selectionAnchor)
|
|
216
274
|
this.deleteSelectedText();
|
|
217
275
|
else
|
|
218
276
|
this.deleteForward();
|
|
219
277
|
return true;
|
|
220
278
|
case KEYS.TAB:
|
|
279
|
+
this.clearSearchResults();
|
|
221
280
|
this.insertSoftTab();
|
|
222
281
|
return true;
|
|
223
282
|
// --- Search & History ---
|
|
@@ -273,6 +332,7 @@ function handleEditKeys(key) {
|
|
|
273
332
|
// Xử lý Ký tự in được
|
|
274
333
|
default:
|
|
275
334
|
if (key.length === 1 && key >= ' ' && key <= '~') {
|
|
335
|
+
this.clearSearchResults();
|
|
276
336
|
this.handleCharacterKey(key);
|
|
277
337
|
return true;
|
|
278
338
|
}
|
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;
|
|
@@ -58,6 +63,9 @@ function render() {
|
|
|
58
63
|
: ' '.padStart(this.gutterWidth - 2, ' ') + ' | ';
|
|
59
64
|
buffer += lineNumber;
|
|
60
65
|
let lineContent = row.content;
|
|
66
|
+
// Retrieve syntax color map for the full logical line
|
|
67
|
+
// We pass the full line content because the scanner needs context
|
|
68
|
+
const syntaxColorMap = this.getLineSyntaxColor(row.logicalY, this.lines[row.logicalY]);
|
|
61
69
|
// 2. Draw Content (Character by Character for selection/cursor)
|
|
62
70
|
for (let i = 0; i < lineContent.length; i++) {
|
|
63
71
|
const char = lineContent[i];
|
|
@@ -66,10 +74,24 @@ function render() {
|
|
|
66
74
|
const isCursorPosition = (visualRowIndex === currentVisualRowIndex && i === cursorVisualX);
|
|
67
75
|
const isSelected = selectionRange && this.isPositionInSelection(logicalY, logicalX, selectionRange);
|
|
68
76
|
// Highlight search result under cursor
|
|
69
|
-
|
|
77
|
+
// Check if this character is part of ANY search result
|
|
78
|
+
let isGlobalSearchResult = false;
|
|
79
|
+
if (this.searchResultMap.has(logicalY)) {
|
|
80
|
+
const matches = this.searchResultMap.get(logicalY);
|
|
81
|
+
for (const match of matches) {
|
|
82
|
+
if (logicalX >= match.start && logicalX < match.end) {
|
|
83
|
+
isGlobalSearchResult = true;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Check if this character is part of the CURRENTLY SELECTED search result
|
|
89
|
+
const isCurrentSearchResult = (this.searchResultIndex !== -1 &&
|
|
70
90
|
this.searchResults[this.searchResultIndex]?.y === logicalY &&
|
|
71
91
|
logicalX >= this.searchResults[this.searchResultIndex]?.x &&
|
|
72
92
|
logicalX < (this.searchResults[this.searchResultIndex]?.x + this.searchQuery.length));
|
|
93
|
+
// Syntax highlight color
|
|
94
|
+
const syntaxColor = syntaxColorMap.get(logicalX);
|
|
73
95
|
if (isSelected) {
|
|
74
96
|
buffer += ANSI.INVERT_COLORS + char + ANSI.RESET_COLORS;
|
|
75
97
|
}
|
|
@@ -77,10 +99,18 @@ function render() {
|
|
|
77
99
|
// Cursor is a single inverted character if not already covered by selection
|
|
78
100
|
buffer += ANSI.INVERT_COLORS + char + ANSI.RESET_COLORS;
|
|
79
101
|
}
|
|
80
|
-
else if (
|
|
81
|
-
//
|
|
102
|
+
else if (isCurrentSearchResult) {
|
|
103
|
+
// Selected Match: Invert + Underline (if supported) or just Invert
|
|
104
|
+
buffer += ANSI.INVERT_COLORS + '\x1b[4m' + char + ANSI.RESET_COLORS;
|
|
105
|
+
}
|
|
106
|
+
else if (isGlobalSearchResult) {
|
|
107
|
+
// Global Match: Invert only
|
|
82
108
|
buffer += ANSI.INVERT_COLORS + char + ANSI.RESET_COLORS;
|
|
83
109
|
}
|
|
110
|
+
else if (syntaxColor) {
|
|
111
|
+
// Apply syntax color
|
|
112
|
+
buffer += syntaxColor + char + ANSI.RESET_COLORS;
|
|
113
|
+
}
|
|
84
114
|
else {
|
|
85
115
|
buffer += char;
|
|
86
116
|
}
|
|
@@ -93,6 +123,13 @@ function render() {
|
|
|
93
123
|
}
|
|
94
124
|
buffer += `${ANSI.CLEAR_LINE}`;
|
|
95
125
|
}
|
|
126
|
+
// Draw Scrollbar (Phase 2)
|
|
127
|
+
if (showScrollbar) {
|
|
128
|
+
const isThumb = y >= thumbStart && y < thumbStart + thumbHeight;
|
|
129
|
+
const scrollChar = isThumb ? '┃' : '│';
|
|
130
|
+
// Move to last column and draw
|
|
131
|
+
buffer += `\x1b[${this.screenStartRow + y};${this.screenCols}H${ANSI.RESET_COLORS}${scrollChar}`;
|
|
132
|
+
}
|
|
96
133
|
}
|
|
97
134
|
// Draw status bar
|
|
98
135
|
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,16 @@
|
|
|
1
|
+
import { CliEditor } from './editor.js';
|
|
2
|
+
/**
|
|
3
|
+
* Single-pass character scanner to generate a color map for a line.
|
|
4
|
+
* Implements "Poor Man's Syntax Highlighting" focusing on Brackets and Quotes.
|
|
5
|
+
*/
|
|
6
|
+
declare function getLineSyntaxColor(this: CliEditor, lineIndex: number, lineContent: string): Map<number, string>;
|
|
7
|
+
/**
|
|
8
|
+
* Invalidates the syntax highlighting cache.
|
|
9
|
+
* Clears the entire cache to be safe and simple ("Poor Man's" approach).
|
|
10
|
+
*/
|
|
11
|
+
declare function invalidateSyntaxCache(this: CliEditor): void;
|
|
12
|
+
export declare const syntaxMethods: {
|
|
13
|
+
getLineSyntaxColor: typeof getLineSyntaxColor;
|
|
14
|
+
invalidateSyntaxCache: typeof invalidateSyntaxCache;
|
|
15
|
+
};
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ANSI } from './constants.js';
|
|
2
|
+
// Syntax colors
|
|
3
|
+
const COLOR_BRACKET_1 = ANSI.YELLOW;
|
|
4
|
+
const COLOR_STRING = ANSI.GREEN;
|
|
5
|
+
// State constants
|
|
6
|
+
const STATE_NORMAL = 0;
|
|
7
|
+
const STATE_IN_STRING_SINGLE = 1; // '
|
|
8
|
+
const STATE_IN_STRING_DOUBLE = 2; // "
|
|
9
|
+
/**
|
|
10
|
+
* Single-pass character scanner to generate a color map for a line.
|
|
11
|
+
* Implements "Poor Man's Syntax Highlighting" focusing on Brackets and Quotes.
|
|
12
|
+
*/
|
|
13
|
+
function getLineSyntaxColor(lineIndex, lineContent) {
|
|
14
|
+
// Check cache first
|
|
15
|
+
if (this.syntaxCache.has(lineIndex)) {
|
|
16
|
+
return this.syntaxCache.get(lineIndex);
|
|
17
|
+
}
|
|
18
|
+
const colorMap = new Map();
|
|
19
|
+
let state = STATE_NORMAL;
|
|
20
|
+
for (let i = 0; i < lineContent.length; i++) {
|
|
21
|
+
const char = lineContent[i];
|
|
22
|
+
if (state === STATE_NORMAL) {
|
|
23
|
+
if (char === '"') {
|
|
24
|
+
state = STATE_IN_STRING_DOUBLE;
|
|
25
|
+
colorMap.set(i, COLOR_STRING);
|
|
26
|
+
}
|
|
27
|
+
else if (char === "'") {
|
|
28
|
+
state = STATE_IN_STRING_SINGLE;
|
|
29
|
+
colorMap.set(i, COLOR_STRING);
|
|
30
|
+
}
|
|
31
|
+
else if ('()[]{}'.includes(char)) {
|
|
32
|
+
// Alternate bracket colors for fun, or just use one
|
|
33
|
+
colorMap.set(i, COLOR_BRACKET_1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (state === STATE_IN_STRING_DOUBLE) {
|
|
37
|
+
colorMap.set(i, COLOR_STRING);
|
|
38
|
+
if (char === '"') {
|
|
39
|
+
// Check if escaped
|
|
40
|
+
let backslashCount = 0;
|
|
41
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
42
|
+
if (lineContent[j] === '\\')
|
|
43
|
+
backslashCount++;
|
|
44
|
+
else
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
// Even backslashes => not escaped (e.g., \\" is literal backslash then quote)
|
|
48
|
+
// Odd backslashes => escaped (e.g., \" is literal quote)
|
|
49
|
+
if (backslashCount % 2 === 0) {
|
|
50
|
+
state = STATE_NORMAL;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else if (state === STATE_IN_STRING_SINGLE) {
|
|
55
|
+
colorMap.set(i, COLOR_STRING);
|
|
56
|
+
if (char === "'") {
|
|
57
|
+
// Check if escaped
|
|
58
|
+
let backslashCount = 0;
|
|
59
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
60
|
+
if (lineContent[j] === '\\')
|
|
61
|
+
backslashCount++;
|
|
62
|
+
else
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
if (backslashCount % 2 === 0) {
|
|
66
|
+
state = STATE_NORMAL;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
this.syntaxCache.set(lineIndex, colorMap);
|
|
72
|
+
return colorMap;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Invalidates the syntax highlighting cache.
|
|
76
|
+
* Clears the entire cache to be safe and simple ("Poor Man's" approach).
|
|
77
|
+
*/
|
|
78
|
+
function invalidateSyntaxCache() {
|
|
79
|
+
this.syntaxCache.clear();
|
|
80
|
+
}
|
|
81
|
+
export const syntaxMethods = {
|
|
82
|
+
getLineSyntaxColor,
|
|
83
|
+
invalidateSyntaxCache
|
|
84
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Defines the interface for a keypress event.
|
|
3
|
+
* Adapted from the editor.ts file.
|
|
4
4
|
*/
|
|
5
5
|
export interface KeypressEvent {
|
|
6
6
|
name?: string;
|
|
@@ -11,7 +11,7 @@ export interface KeypressEvent {
|
|
|
11
11
|
code?: string;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Main function, accepts a Readable Stream and makes it
|
|
15
|
+
* emit "keypress" events.
|
|
16
16
|
*/
|
|
17
17
|
export default function keypress(stream: NodeJS.ReadStream): void;
|
package/dist/vendor/keypress.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// src/vendor/keypress.ts
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// This is a "vendored" version of the 'keypress' library (0.2.1)
|
|
3
|
+
// converted to TypeScript and stripped of mouse support
|
|
4
|
+
// to be integrated directly into cliedit.
|
|
5
5
|
import { EventEmitter } from 'events';
|
|
6
6
|
import { StringDecoder } from 'string_decoder';
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Polyfill for `EventEmitter.listenerCount()`, for backward compatibility.
|
|
9
9
|
*/
|
|
10
10
|
let listenerCount = EventEmitter.listenerCount;
|
|
11
11
|
if (!listenerCount) {
|
|
@@ -14,18 +14,19 @@ if (!listenerCount) {
|
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Regexes
|
|
17
|
+
* Regexes used to parse ansi escape codes.
|
|
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
|
-
*
|
|
23
|
+
* Main function, accepts a Readable Stream and makes it
|
|
24
|
+
* emit "keypress" events.
|
|
24
25
|
*/
|
|
25
26
|
export default function keypress(stream) {
|
|
26
27
|
if (isEmittingKeypress(stream))
|
|
27
28
|
return;
|
|
28
|
-
//
|
|
29
|
+
// Attach decoder to the stream to monitor data
|
|
29
30
|
stream._keypressDecoder = new StringDecoder('utf8');
|
|
30
31
|
function onData(b) {
|
|
31
32
|
if (listenerCount(stream, 'keypress') > 0) {
|
|
@@ -34,7 +35,7 @@ export default function keypress(stream) {
|
|
|
34
35
|
emitKey(stream, r);
|
|
35
36
|
}
|
|
36
37
|
else {
|
|
37
|
-
//
|
|
38
|
+
// No one is listening, remove listener
|
|
38
39
|
stream.removeListener('data', onData);
|
|
39
40
|
stream.on('newListener', onNewListener);
|
|
40
41
|
}
|
|
@@ -53,23 +54,23 @@ export default function keypress(stream) {
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
56
|
-
*
|
|
57
|
+
* Checks if the stream has already emitted the "keypress" event.
|
|
57
58
|
*/
|
|
58
59
|
function isEmittingKeypress(stream) {
|
|
59
60
|
let rtn = !!stream._keypressDecoder;
|
|
60
61
|
if (!rtn) {
|
|
61
|
-
// XXX:
|
|
62
|
-
//
|
|
63
|
-
//
|
|
62
|
+
// XXX: For older node versions, we want to remove existing
|
|
63
|
+
// "data" and "newListener" listeners because they won't
|
|
64
|
+
// include extensions from this module (like "mousepress" which was removed).
|
|
64
65
|
stream.listeners('data').slice(0).forEach(function (l) {
|
|
65
66
|
if (l.name === 'onData' && /emitKey/.test(l.toString())) {
|
|
66
|
-
// FIX TS2769:
|
|
67
|
+
// FIX TS2769: Cast 'l' to a valid listener type
|
|
67
68
|
stream.removeListener('data', l);
|
|
68
69
|
}
|
|
69
70
|
});
|
|
70
71
|
stream.listeners('newListener').slice(0).forEach(function (l) {
|
|
71
72
|
if (l.name === 'onNewListener' && /keypress/.test(l.toString())) {
|
|
72
|
-
// FIX TS2769:
|
|
73
|
+
// FIX TS2769: Cast 'l' to a valid listener type
|
|
73
74
|
stream.removeListener('newListener', l);
|
|
74
75
|
}
|
|
75
76
|
});
|
|
@@ -77,8 +78,8 @@ function isEmittingKeypress(stream) {
|
|
|
77
78
|
return rtn;
|
|
78
79
|
}
|
|
79
80
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
81
|
+
* The code below is taken from node-core's `readline.js` module
|
|
82
|
+
* and has been converted to TypeScript.
|
|
82
83
|
*/
|
|
83
84
|
function emitKey(stream, s) {
|
|
84
85
|
let ch;
|
|
@@ -90,16 +91,16 @@ function emitKey(stream, s) {
|
|
|
90
91
|
sequence: s,
|
|
91
92
|
};
|
|
92
93
|
let parts;
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
94
|
+
// Warning: The `Buffer.isBuffer(s)` block has been removed.
|
|
95
|
+
// Reason: `onData` always calls `emitKey` with a string (result from StringDecoder).
|
|
96
|
+
// The recursive block (paste) also calls with a string.
|
|
97
|
+
// Therefore, `s` is always a string.
|
|
97
98
|
if (s === '\r') {
|
|
98
99
|
// carriage return
|
|
99
100
|
key.name = 'return';
|
|
100
101
|
}
|
|
101
102
|
else if (s === '\n') {
|
|
102
|
-
// enter,
|
|
103
|
+
// enter, should have been linefeed
|
|
103
104
|
key.name = 'enter';
|
|
104
105
|
}
|
|
105
106
|
else if (s === '\t') {
|
|
@@ -110,7 +111,7 @@ function emitKey(stream, s) {
|
|
|
110
111
|
s === '\x7f' ||
|
|
111
112
|
s === '\x1b\x7f' ||
|
|
112
113
|
s === '\x1b\b') {
|
|
113
|
-
// backspace
|
|
114
|
+
// backspace or ctrl+h
|
|
114
115
|
key.name = 'backspace';
|
|
115
116
|
key.meta = s.charAt(0) === '\x1b';
|
|
116
117
|
}
|
|
@@ -155,20 +156,20 @@ function emitKey(stream, s) {
|
|
|
155
156
|
}
|
|
156
157
|
else if ((parts = functionKeyCodeRe.exec(s))) {
|
|
157
158
|
// ansi escape sequence
|
|
158
|
-
//
|
|
159
|
-
// bitflag
|
|
159
|
+
// Reassemble key code, ignoring leading \x1b,
|
|
160
|
+
// modifier bitflag, and any meaningless "1;" strings
|
|
160
161
|
const code = (parts[1] || '') +
|
|
161
162
|
(parts[2] || '') +
|
|
162
163
|
(parts[4] || '') +
|
|
163
164
|
(parts[6] || '');
|
|
164
|
-
// FIX TS2362:
|
|
165
|
+
// FIX TS2362: Convert (parts[...]) to number using parseInt
|
|
165
166
|
const modifier = parseInt(parts[3] || parts[5] || '1', 10) - 1;
|
|
166
|
-
//
|
|
167
|
+
// Parse modifier keys
|
|
167
168
|
key.ctrl = !!(modifier & 4);
|
|
168
169
|
key.meta = !!(modifier & 10);
|
|
169
170
|
key.shift = !!(modifier & 1);
|
|
170
171
|
key.code = code;
|
|
171
|
-
//
|
|
172
|
+
// Parse the key itself
|
|
172
173
|
switch (code) {
|
|
173
174
|
/* xterm/gnome ESC O letter */
|
|
174
175
|
case 'OP':
|
|
@@ -414,15 +415,40 @@ function emitKey(stream, s) {
|
|
|
414
415
|
}
|
|
415
416
|
}
|
|
416
417
|
else if (s.length > 1 && s[0] !== '\x1b') {
|
|
417
|
-
//
|
|
418
|
-
//
|
|
418
|
+
// Received a string longer than one character.
|
|
419
|
+
// Could be a paste, since it's not a control sequence.
|
|
419
420
|
for (const c of s) {
|
|
420
421
|
emitKey(stream, c);
|
|
421
422
|
}
|
|
422
423
|
return;
|
|
423
424
|
}
|
|
424
|
-
//
|
|
425
|
-
|
|
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
|
+
}
|
|
451
|
+
// Don't emit key if name is not found
|
|
426
452
|
if (key.name === undefined) {
|
|
427
453
|
return; // key = undefined;
|
|
428
454
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cliedit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "A lightweight, raw-mode terminal editor utility for Node.js CLI applications, with line wrapping and undo/redo support.",
|
|
5
|
-
"repository":
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/CodeTease/cliedit.git"
|
|
8
|
+
},
|
|
6
9
|
"type": "module",
|
|
7
10
|
"main": "dist/index.js",
|
|
8
11
|
"types": "dist/index.d.ts",
|
|
@@ -21,7 +24,7 @@
|
|
|
21
24
|
"scripts": {
|
|
22
25
|
"build": "tsc",
|
|
23
26
|
"prepublishOnly": "npm run build",
|
|
24
|
-
"demo": "npm run build && cross-env NODE_ENV=development
|
|
27
|
+
"demo": "npm run build && cross-env NODE_ENV=development tsx src/demo.ts",
|
|
25
28
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
26
29
|
},
|
|
27
30
|
"keywords": [
|
|
@@ -35,10 +38,9 @@
|
|
|
35
38
|
"author": "CodeTease",
|
|
36
39
|
"license": "MIT",
|
|
37
40
|
"devDependencies": {
|
|
38
|
-
"@types/node": "^
|
|
39
|
-
"cross-env": "^7
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
"dependencies": {
|
|
41
|
+
"@types/node": "^22",
|
|
42
|
+
"cross-env": "^7",
|
|
43
|
+
"tsx": "^4",
|
|
44
|
+
"typescript": "^5"
|
|
43
45
|
}
|
|
44
|
-
}
|
|
46
|
+
}
|