@boba-cli/textarea 0.1.0-alpha.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.
- package/README.md +73 -0
- package/dist/index.cjs +686 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +184 -0
- package/dist/index.d.ts +184 -0
- package/dist/index.js +672 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chapstick = require('@boba-cli/chapstick');
|
|
4
|
+
var cursor = require('@boba-cli/cursor');
|
|
5
|
+
var key = require('@boba-cli/key');
|
|
6
|
+
var tea = require('@boba-cli/tea');
|
|
7
|
+
var runeutil = require('@boba-cli/runeutil');
|
|
8
|
+
var GraphemeSplitter = require('grapheme-splitter');
|
|
9
|
+
var clipboard = require('clipboardy');
|
|
10
|
+
|
|
11
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
|
|
13
|
+
var GraphemeSplitter__default = /*#__PURE__*/_interopDefault(GraphemeSplitter);
|
|
14
|
+
var clipboard__default = /*#__PURE__*/_interopDefault(clipboard);
|
|
15
|
+
|
|
16
|
+
// src/model.ts
|
|
17
|
+
var PasteMsg = class {
|
|
18
|
+
constructor(text) {
|
|
19
|
+
this.text = text;
|
|
20
|
+
}
|
|
21
|
+
_tag = "textarea/paste";
|
|
22
|
+
};
|
|
23
|
+
var PasteErrorMsg = class {
|
|
24
|
+
constructor(error) {
|
|
25
|
+
this.error = error;
|
|
26
|
+
}
|
|
27
|
+
_tag = "textarea/paste-error";
|
|
28
|
+
};
|
|
29
|
+
function pasteCommand() {
|
|
30
|
+
return async () => {
|
|
31
|
+
try {
|
|
32
|
+
const text = await clipboard__default.default.read();
|
|
33
|
+
return new PasteMsg(text ?? "");
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return new PasteErrorMsg(error);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
var defaultKeyMap = {
|
|
40
|
+
characterLeft: key.newBinding({ keys: ["left"] }),
|
|
41
|
+
characterRight: key.newBinding({ keys: ["right"] }),
|
|
42
|
+
lineUp: key.newBinding({ keys: ["up", "k"] }),
|
|
43
|
+
lineDown: key.newBinding({ keys: ["down", "j"] }),
|
|
44
|
+
lineStart: key.newBinding({ keys: ["home", "ctrl+a"] }),
|
|
45
|
+
lineEnd: key.newBinding({ keys: ["end", "ctrl+e"] }),
|
|
46
|
+
insertNewline: key.newBinding({ keys: ["enter"] }),
|
|
47
|
+
deleteCharBackward: key.newBinding({ keys: ["backspace", "ctrl+h"] }),
|
|
48
|
+
deleteCharForward: key.newBinding({ keys: ["delete", "ctrl+d"] }),
|
|
49
|
+
deleteLine: key.newBinding({ keys: ["ctrl+u"] }),
|
|
50
|
+
gotoLineStart: key.newBinding({ keys: ["home"] }),
|
|
51
|
+
gotoLineEnd: key.newBinding({ keys: ["end"] }),
|
|
52
|
+
paste: key.newBinding({ keys: ["ctrl+v"] })
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// src/model.ts
|
|
56
|
+
var splitter = new GraphemeSplitter__default.default();
|
|
57
|
+
var sanitizer = runeutil.newSanitizer();
|
|
58
|
+
var TextareaModel = class _TextareaModel {
|
|
59
|
+
lines;
|
|
60
|
+
pos;
|
|
61
|
+
focused;
|
|
62
|
+
width;
|
|
63
|
+
maxHeight;
|
|
64
|
+
maxWidth;
|
|
65
|
+
prompt;
|
|
66
|
+
promptStyle;
|
|
67
|
+
textStyle;
|
|
68
|
+
placeholderStyle;
|
|
69
|
+
cursorStyle;
|
|
70
|
+
lineNumberStyle;
|
|
71
|
+
showLineNumbers;
|
|
72
|
+
endOfBufferCharacter;
|
|
73
|
+
validateFn;
|
|
74
|
+
keyMap;
|
|
75
|
+
cursor;
|
|
76
|
+
error;
|
|
77
|
+
scrollTop;
|
|
78
|
+
placeholder;
|
|
79
|
+
constructor(state) {
|
|
80
|
+
this.lines = state.lines;
|
|
81
|
+
this.pos = state.pos;
|
|
82
|
+
this.focused = state.focused;
|
|
83
|
+
this.width = state.width;
|
|
84
|
+
this.maxHeight = state.maxHeight;
|
|
85
|
+
this.maxWidth = state.maxWidth;
|
|
86
|
+
this.prompt = state.prompt;
|
|
87
|
+
this.promptStyle = state.promptStyle;
|
|
88
|
+
this.textStyle = state.textStyle;
|
|
89
|
+
this.placeholderStyle = state.placeholderStyle;
|
|
90
|
+
this.cursorStyle = state.cursorStyle;
|
|
91
|
+
this.lineNumberStyle = state.lineNumberStyle;
|
|
92
|
+
this.showLineNumbers = state.showLineNumbers;
|
|
93
|
+
this.endOfBufferCharacter = state.endOfBufferCharacter;
|
|
94
|
+
this.validateFn = state.validateFn;
|
|
95
|
+
this.keyMap = state.keyMap;
|
|
96
|
+
this.cursor = state.cursor;
|
|
97
|
+
this.error = state.error;
|
|
98
|
+
this.scrollTop = state.scrollTop;
|
|
99
|
+
this.placeholder = state.placeholder;
|
|
100
|
+
}
|
|
101
|
+
/** Create a new textarea model. */
|
|
102
|
+
static new(options = {}) {
|
|
103
|
+
const rawValue = options.value ?? "";
|
|
104
|
+
const sanitized = sanitize(rawValue);
|
|
105
|
+
const lines = splitLines(sanitized);
|
|
106
|
+
const cursorMode = options.cursorMode ?? cursor.CursorMode.Blink;
|
|
107
|
+
const textStyle = options.textStyle ?? new chapstick.Style();
|
|
108
|
+
const cursor$1 = new cursor.CursorModel({
|
|
109
|
+
style: options.cursorStyle ?? new chapstick.Style(),
|
|
110
|
+
textStyle,
|
|
111
|
+
mode: cursorMode,
|
|
112
|
+
focused: false,
|
|
113
|
+
char: "\u258C"
|
|
114
|
+
});
|
|
115
|
+
const model = new _TextareaModel({
|
|
116
|
+
lines: lines.length > 0 ? lines : [""],
|
|
117
|
+
pos: {
|
|
118
|
+
line: Math.max(0, lines.length - 1),
|
|
119
|
+
column: lines.length > 0 ? runeCount(last(lines) ?? "") : 0
|
|
120
|
+
},
|
|
121
|
+
focused: false,
|
|
122
|
+
width: options.width ?? 0,
|
|
123
|
+
maxHeight: options.maxHeight ?? 0,
|
|
124
|
+
maxWidth: options.maxWidth ?? 0,
|
|
125
|
+
prompt: options.prompt ?? "",
|
|
126
|
+
promptStyle: options.promptStyle ?? new chapstick.Style(),
|
|
127
|
+
textStyle,
|
|
128
|
+
placeholderStyle: options.placeholderStyle ?? new chapstick.Style(),
|
|
129
|
+
cursorStyle: options.cursorStyle ?? new chapstick.Style(),
|
|
130
|
+
lineNumberStyle: options.lineNumberStyle ?? new chapstick.Style(),
|
|
131
|
+
showLineNumbers: options.showLineNumbers ?? false,
|
|
132
|
+
endOfBufferCharacter: options.endOfBufferCharacter ?? "~",
|
|
133
|
+
validateFn: options.validate,
|
|
134
|
+
keyMap: options.keyMap ?? defaultKeyMap,
|
|
135
|
+
cursor: cursor$1,
|
|
136
|
+
error: null,
|
|
137
|
+
scrollTop: 0,
|
|
138
|
+
placeholder: options.placeholder
|
|
139
|
+
});
|
|
140
|
+
return model.#withError(model.#runValidation(model.value()));
|
|
141
|
+
}
|
|
142
|
+
/** Initial command (none by default). */
|
|
143
|
+
init() {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
/** Full text value. */
|
|
147
|
+
value() {
|
|
148
|
+
return this.lines.join("\n");
|
|
149
|
+
}
|
|
150
|
+
/** Number of lines. */
|
|
151
|
+
lineCount() {
|
|
152
|
+
return this.lines.length;
|
|
153
|
+
}
|
|
154
|
+
/** Current line index (0-based). */
|
|
155
|
+
currentLine() {
|
|
156
|
+
return this.pos.line;
|
|
157
|
+
}
|
|
158
|
+
/** Current column (grapheme index). */
|
|
159
|
+
currentColumn() {
|
|
160
|
+
return this.pos.column;
|
|
161
|
+
}
|
|
162
|
+
/** Content of a line (empty string if out of bounds). */
|
|
163
|
+
lineContent(line) {
|
|
164
|
+
return this.lines[line] ?? "";
|
|
165
|
+
}
|
|
166
|
+
/** Is textarea empty. */
|
|
167
|
+
isEmpty() {
|
|
168
|
+
return this.lines.length === 1 && this.lines[0]?.length === 0;
|
|
169
|
+
}
|
|
170
|
+
/** Focus the textarea (enables key handling). */
|
|
171
|
+
focus() {
|
|
172
|
+
if (this.focused) {
|
|
173
|
+
return [this, null];
|
|
174
|
+
}
|
|
175
|
+
const [cursor, cmd] = this.cursor.focus();
|
|
176
|
+
const next = this.#with({ focused: true, cursor });
|
|
177
|
+
return [next, cmd];
|
|
178
|
+
}
|
|
179
|
+
/** Blur the textarea. */
|
|
180
|
+
blur() {
|
|
181
|
+
if (!this.focused) {
|
|
182
|
+
return this;
|
|
183
|
+
}
|
|
184
|
+
return this.#with({ focused: false, cursor: this.cursor.blur() });
|
|
185
|
+
}
|
|
186
|
+
/** Move cursor to start of current line. */
|
|
187
|
+
gotoLineStart() {
|
|
188
|
+
return this.#withPosition({ line: this.pos.line, column: 0 });
|
|
189
|
+
}
|
|
190
|
+
/** Move cursor to end of current line. */
|
|
191
|
+
gotoLineEnd() {
|
|
192
|
+
const len = runeCount(this.lines[this.pos.line] ?? "");
|
|
193
|
+
return this.#withPosition({ line: this.pos.line, column: len });
|
|
194
|
+
}
|
|
195
|
+
/** Move up n lines (default 1). */
|
|
196
|
+
lineUp(n = 1) {
|
|
197
|
+
const target = clamp(
|
|
198
|
+
this.pos.line - Math.max(1, n),
|
|
199
|
+
0,
|
|
200
|
+
this.lines.length - 1
|
|
201
|
+
);
|
|
202
|
+
return this.#withPosition({ line: target, column: this.pos.column });
|
|
203
|
+
}
|
|
204
|
+
/** Move cursor left, possibly across lines. */
|
|
205
|
+
cursorLeft() {
|
|
206
|
+
if (this.pos.column > 0) {
|
|
207
|
+
return this.#withPosition({
|
|
208
|
+
line: this.pos.line,
|
|
209
|
+
column: this.pos.column - 1
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (this.pos.line > 0) {
|
|
213
|
+
const prevLine = this.lines[this.pos.line - 1] ?? "";
|
|
214
|
+
return this.#withPosition({
|
|
215
|
+
line: this.pos.line - 1,
|
|
216
|
+
column: runeCount(prevLine)
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return this;
|
|
220
|
+
}
|
|
221
|
+
/** Move cursor right, possibly across lines. */
|
|
222
|
+
cursorRight() {
|
|
223
|
+
const line = this.lines[this.pos.line] ?? "";
|
|
224
|
+
const len = runeCount(line);
|
|
225
|
+
if (this.pos.column < len) {
|
|
226
|
+
return this.#withPosition({
|
|
227
|
+
line: this.pos.line,
|
|
228
|
+
column: this.pos.column + 1
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (this.pos.line < this.lines.length - 1) {
|
|
232
|
+
return this.#withPosition({ line: this.pos.line + 1, column: 0 });
|
|
233
|
+
}
|
|
234
|
+
return this;
|
|
235
|
+
}
|
|
236
|
+
/** Move down n lines (default 1). */
|
|
237
|
+
lineDown(n = 1) {
|
|
238
|
+
const target = clamp(
|
|
239
|
+
this.pos.line + Math.max(1, n),
|
|
240
|
+
0,
|
|
241
|
+
this.lines.length - 1
|
|
242
|
+
);
|
|
243
|
+
return this.#withPosition({ line: target, column: this.pos.column });
|
|
244
|
+
}
|
|
245
|
+
/** Jump to a specific line (0-based). */
|
|
246
|
+
gotoLine(line) {
|
|
247
|
+
const target = clamp(line, 0, this.lines.length - 1);
|
|
248
|
+
return this.#withPosition({ line: target, column: this.pos.column });
|
|
249
|
+
}
|
|
250
|
+
/** Insert text at cursor (supports multi-line). */
|
|
251
|
+
insertRunes(text) {
|
|
252
|
+
if (!text) {
|
|
253
|
+
return this;
|
|
254
|
+
}
|
|
255
|
+
const sanitized = sanitize(text);
|
|
256
|
+
if (!sanitized) {
|
|
257
|
+
return this;
|
|
258
|
+
}
|
|
259
|
+
const parts = sanitized.split("\n");
|
|
260
|
+
const current = this.lines[this.pos.line] ?? "";
|
|
261
|
+
const before = sliceGraphemes(current, 0, this.pos.column);
|
|
262
|
+
const after = sliceGraphemes(current, this.pos.column);
|
|
263
|
+
let nextLines = [];
|
|
264
|
+
if (parts.length === 1) {
|
|
265
|
+
const merged = before + parts[0] + after;
|
|
266
|
+
nextLines = replaceLine(this.lines, this.pos.line, merged);
|
|
267
|
+
} else {
|
|
268
|
+
const first = before + parts[0];
|
|
269
|
+
const last2 = parts[parts.length - 1];
|
|
270
|
+
const middle = parts.slice(1, -1);
|
|
271
|
+
const tail = last2 + after;
|
|
272
|
+
nextLines = [
|
|
273
|
+
...this.lines.slice(0, this.pos.line),
|
|
274
|
+
first,
|
|
275
|
+
...middle,
|
|
276
|
+
tail,
|
|
277
|
+
...this.lines.slice(this.pos.line + 1)
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
const newLine = this.pos.line + (parts.length - 1);
|
|
281
|
+
const newColumn = parts.length === 1 ? this.pos.column + runeCount(parts[0]) : runeCount(last(parts));
|
|
282
|
+
return this.#withValue(nextLines, { line: newLine, column: newColumn });
|
|
283
|
+
}
|
|
284
|
+
/** Insert a newline at the cursor. */
|
|
285
|
+
insertNewline() {
|
|
286
|
+
return this.insertRunes("\n");
|
|
287
|
+
}
|
|
288
|
+
/** Delete character to the left (backspace). */
|
|
289
|
+
deleteLeft(n = 1) {
|
|
290
|
+
if (n <= 0) return this;
|
|
291
|
+
let model = this;
|
|
292
|
+
for (let i = 0; i < n; i++) {
|
|
293
|
+
const next = model.#deleteLeftOnce();
|
|
294
|
+
if (next === model) break;
|
|
295
|
+
model = next;
|
|
296
|
+
}
|
|
297
|
+
return model;
|
|
298
|
+
}
|
|
299
|
+
#deleteLeftOnce() {
|
|
300
|
+
if (this.pos.column > 0) {
|
|
301
|
+
const line = this.lines[this.pos.line] ?? "";
|
|
302
|
+
const before = sliceGraphemes(line, 0, this.pos.column - 1);
|
|
303
|
+
const after = sliceGraphemes(line, this.pos.column);
|
|
304
|
+
const merged = before + after;
|
|
305
|
+
return this.#withValue(replaceLine(this.lines, this.pos.line, merged), {
|
|
306
|
+
line: this.pos.line,
|
|
307
|
+
column: this.pos.column - 1
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if (this.pos.line === 0) {
|
|
311
|
+
return this;
|
|
312
|
+
}
|
|
313
|
+
const prevLine = this.lines[this.pos.line - 1] ?? "";
|
|
314
|
+
const current = this.lines[this.pos.line] ?? "";
|
|
315
|
+
const mergedLine = prevLine + current;
|
|
316
|
+
const nextLines = [
|
|
317
|
+
...this.lines.slice(0, this.pos.line - 1),
|
|
318
|
+
mergedLine,
|
|
319
|
+
...this.lines.slice(this.pos.line + 1)
|
|
320
|
+
];
|
|
321
|
+
return this.#withValue(nextLines, {
|
|
322
|
+
line: this.pos.line - 1,
|
|
323
|
+
column: runeCount(prevLine)
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
/** Delete character to the right (delete). */
|
|
327
|
+
deleteRight(n = 1) {
|
|
328
|
+
if (n <= 0) return this;
|
|
329
|
+
let model = this;
|
|
330
|
+
for (let i = 0; i < n; i++) {
|
|
331
|
+
const next = model.#deleteRightOnce();
|
|
332
|
+
if (next === model) break;
|
|
333
|
+
model = next;
|
|
334
|
+
}
|
|
335
|
+
return model;
|
|
336
|
+
}
|
|
337
|
+
#deleteRightOnce() {
|
|
338
|
+
const line = this.lines[this.pos.line] ?? "";
|
|
339
|
+
const len = runeCount(line);
|
|
340
|
+
if (this.pos.column < len) {
|
|
341
|
+
const before = sliceGraphemes(line, 0, this.pos.column);
|
|
342
|
+
const after = sliceGraphemes(line, this.pos.column + 1);
|
|
343
|
+
const merged2 = before + after;
|
|
344
|
+
return this.#withValue(replaceLine(this.lines, this.pos.line, merged2), {
|
|
345
|
+
line: this.pos.line,
|
|
346
|
+
column: this.pos.column
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (this.pos.line >= this.lines.length - 1) {
|
|
350
|
+
return this;
|
|
351
|
+
}
|
|
352
|
+
const nextLine = this.lines[this.pos.line + 1] ?? "";
|
|
353
|
+
const merged = line + nextLine;
|
|
354
|
+
const nextLines = [
|
|
355
|
+
...this.lines.slice(0, this.pos.line),
|
|
356
|
+
merged,
|
|
357
|
+
...this.lines.slice(this.pos.line + 2)
|
|
358
|
+
];
|
|
359
|
+
return this.#withValue(nextLines, {
|
|
360
|
+
line: this.pos.line,
|
|
361
|
+
column: this.pos.column
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
/** Delete the current line. Keeps one empty line minimum. */
|
|
365
|
+
deleteLine() {
|
|
366
|
+
if (this.lines.length === 1) {
|
|
367
|
+
return this.reset();
|
|
368
|
+
}
|
|
369
|
+
const nextLines = this.lines.filter((_, i) => i !== this.pos.line);
|
|
370
|
+
const nextLine = clamp(this.pos.line, 0, nextLines.length - 1);
|
|
371
|
+
const nextColumn = clamp(
|
|
372
|
+
this.pos.column,
|
|
373
|
+
0,
|
|
374
|
+
runeCount(nextLines[nextLine] ?? "")
|
|
375
|
+
);
|
|
376
|
+
return this.#withValue(nextLines, { line: nextLine, column: nextColumn });
|
|
377
|
+
}
|
|
378
|
+
/** Duplicate the current line below. */
|
|
379
|
+
duplicateLine() {
|
|
380
|
+
const line = this.lines[this.pos.line] ?? "";
|
|
381
|
+
const nextLines = [
|
|
382
|
+
...this.lines.slice(0, this.pos.line + 1),
|
|
383
|
+
line,
|
|
384
|
+
...this.lines.slice(this.pos.line + 1)
|
|
385
|
+
];
|
|
386
|
+
return this.#withValue(nextLines, {
|
|
387
|
+
line: this.pos.line + 1,
|
|
388
|
+
column: this.pos.column
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
/** Scroll up by n lines (no cursor move). */
|
|
392
|
+
scrollUp(n = 1) {
|
|
393
|
+
const nextScroll = clamp(
|
|
394
|
+
this.scrollTop - Math.max(1, n),
|
|
395
|
+
0,
|
|
396
|
+
this.maxScroll()
|
|
397
|
+
);
|
|
398
|
+
return this.#with({ scrollTop: nextScroll });
|
|
399
|
+
}
|
|
400
|
+
/** Scroll down by n lines (no cursor move). */
|
|
401
|
+
scrollDown(n = 1) {
|
|
402
|
+
const nextScroll = clamp(
|
|
403
|
+
this.scrollTop + Math.max(1, n),
|
|
404
|
+
0,
|
|
405
|
+
this.maxScroll()
|
|
406
|
+
);
|
|
407
|
+
return this.#with({ scrollTop: nextScroll });
|
|
408
|
+
}
|
|
409
|
+
/** Reset to empty content. */
|
|
410
|
+
reset() {
|
|
411
|
+
return this.#withValue([""], { line: 0, column: 0 });
|
|
412
|
+
}
|
|
413
|
+
/** Set a new value (replaces all lines). */
|
|
414
|
+
setValue(value) {
|
|
415
|
+
const sanitized = sanitize(value ?? "");
|
|
416
|
+
const lines = splitLines(sanitized);
|
|
417
|
+
return this.#withValue(lines.length > 0 ? lines : [""], {
|
|
418
|
+
line: Math.max(0, lines.length - 1),
|
|
419
|
+
column: lines.length ? runeCount(last(lines) ?? "") : 0
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
/** Validation function setter. */
|
|
423
|
+
setValidateFunc(fn) {
|
|
424
|
+
const next = this.#with({ validateFn: fn });
|
|
425
|
+
return next.#withError(next.#runValidation(next.value()));
|
|
426
|
+
}
|
|
427
|
+
/** Validate current value. */
|
|
428
|
+
validate() {
|
|
429
|
+
return this.#runValidation(this.value());
|
|
430
|
+
}
|
|
431
|
+
/** Command to paste from clipboard. */
|
|
432
|
+
paste() {
|
|
433
|
+
return [this, pasteCommand()];
|
|
434
|
+
}
|
|
435
|
+
/** Tea update loop. */
|
|
436
|
+
update(msg) {
|
|
437
|
+
let model = this;
|
|
438
|
+
const cmds = [];
|
|
439
|
+
if (msg instanceof tea.KeyMsg) {
|
|
440
|
+
if (model.focused) {
|
|
441
|
+
const [nextModel, cmd] = model.#handleKey(msg);
|
|
442
|
+
model = nextModel;
|
|
443
|
+
if (cmd) cmds.push(cmd);
|
|
444
|
+
}
|
|
445
|
+
} else if (msg instanceof PasteMsg) {
|
|
446
|
+
model = model.insertRunes(msg.text);
|
|
447
|
+
} else if (msg instanceof PasteErrorMsg) {
|
|
448
|
+
model = model.#withError(asError(msg.error));
|
|
449
|
+
}
|
|
450
|
+
const [cursor, cursorCmd] = model.cursor.update(msg);
|
|
451
|
+
model = model.#with({ cursor });
|
|
452
|
+
if (cursorCmd) {
|
|
453
|
+
cmds.push(cursorCmd);
|
|
454
|
+
}
|
|
455
|
+
if (msg instanceof tea.KeyMsg && (msg.key.type === tea.KeyType.Left || msg.key.type === tea.KeyType.Right || msg.key.type === tea.KeyType.Home || msg.key.type === tea.KeyType.End)) {
|
|
456
|
+
const refreshed = model.cursor.withChar(
|
|
457
|
+
model.cursor.isBlinkHidden() ? "\u258C" : model.cursor.char
|
|
458
|
+
);
|
|
459
|
+
model = model.#with({ cursor: refreshed });
|
|
460
|
+
}
|
|
461
|
+
return [model, tea.batch(...cmds)];
|
|
462
|
+
}
|
|
463
|
+
/** Render textarea with cursor and optional line numbers. */
|
|
464
|
+
view() {
|
|
465
|
+
if (this.isEmpty() && this.placeholder) {
|
|
466
|
+
return this.#placeholderView();
|
|
467
|
+
}
|
|
468
|
+
const height = this.viewportHeight();
|
|
469
|
+
const start = clamp(this.scrollTop, 0, Math.max(0, this.lineCount() - 1));
|
|
470
|
+
const visible = this.lines.slice(start, start + height);
|
|
471
|
+
const linesOut = visible.map((line, idx) => {
|
|
472
|
+
const lineIndex = start + idx;
|
|
473
|
+
const isCursorLine = lineIndex === this.pos.line;
|
|
474
|
+
return this.#renderLine(line, lineIndex, isCursorLine);
|
|
475
|
+
});
|
|
476
|
+
const remaining = height - visible.length;
|
|
477
|
+
if (remaining > 0) {
|
|
478
|
+
for (let i = 0; i < remaining; i++) {
|
|
479
|
+
const filler = this.endOfBufferCharacter;
|
|
480
|
+
const prefix = this.#linePrefix(start + visible.length + i, false);
|
|
481
|
+
linesOut.push(prefix + filler);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return linesOut.join("\n");
|
|
485
|
+
}
|
|
486
|
+
#handleKey(msg) {
|
|
487
|
+
let next = this;
|
|
488
|
+
let cmd = null;
|
|
489
|
+
const keyMap = this.keyMap;
|
|
490
|
+
switch (true) {
|
|
491
|
+
case key.matches(msg, keyMap.lineUp):
|
|
492
|
+
next = next.lineUp();
|
|
493
|
+
break;
|
|
494
|
+
case key.matches(msg, keyMap.lineDown):
|
|
495
|
+
next = next.lineDown();
|
|
496
|
+
break;
|
|
497
|
+
case key.matches(msg, keyMap.characterLeft):
|
|
498
|
+
next = next.cursorLeft();
|
|
499
|
+
break;
|
|
500
|
+
case key.matches(msg, keyMap.characterRight):
|
|
501
|
+
next = next.cursorRight();
|
|
502
|
+
break;
|
|
503
|
+
case key.matches(msg, keyMap.lineStart):
|
|
504
|
+
case key.matches(msg, keyMap.gotoLineStart):
|
|
505
|
+
next = next.gotoLineStart();
|
|
506
|
+
break;
|
|
507
|
+
case key.matches(msg, keyMap.lineEnd):
|
|
508
|
+
case key.matches(msg, keyMap.gotoLineEnd):
|
|
509
|
+
next = next.gotoLineEnd();
|
|
510
|
+
break;
|
|
511
|
+
case key.matches(msg, keyMap.insertNewline):
|
|
512
|
+
next = next.insertNewline();
|
|
513
|
+
break;
|
|
514
|
+
case key.matches(msg, keyMap.deleteCharBackward):
|
|
515
|
+
next = next.deleteLeft();
|
|
516
|
+
break;
|
|
517
|
+
case key.matches(msg, keyMap.deleteCharForward):
|
|
518
|
+
next = next.deleteRight();
|
|
519
|
+
break;
|
|
520
|
+
case key.matches(msg, keyMap.deleteLine):
|
|
521
|
+
next = next.deleteLine();
|
|
522
|
+
break;
|
|
523
|
+
case key.matches(msg, keyMap.paste):
|
|
524
|
+
cmd = pasteCommand();
|
|
525
|
+
break;
|
|
526
|
+
default:
|
|
527
|
+
if (msg.key.type === tea.KeyType.Enter) {
|
|
528
|
+
next = next.insertNewline();
|
|
529
|
+
} else if ((msg.key.type === tea.KeyType.Runes || msg.key.type === tea.KeyType.Space) && !msg.key.alt) {
|
|
530
|
+
next = next.insertRunes(msg.key.runes);
|
|
531
|
+
}
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
return [next.#ensureCursorVisible(), cmd];
|
|
535
|
+
}
|
|
536
|
+
#placeholderView() {
|
|
537
|
+
const prefix = this.#linePrefix(0, true);
|
|
538
|
+
const graphemes = splitter.splitGraphemes(this.placeholder ?? "");
|
|
539
|
+
const cursorChar = graphemes[0] ?? "\u258C";
|
|
540
|
+
const rest = graphemes.slice(1).join("");
|
|
541
|
+
const cursorView = this.cursor.withChar(cursorChar).view();
|
|
542
|
+
const body = this.placeholderStyle.inline(true).render(rest);
|
|
543
|
+
return prefix + cursorView + body;
|
|
544
|
+
}
|
|
545
|
+
#renderLine(line, index, isCursorLine) {
|
|
546
|
+
const prefix = this.#linePrefix(index, isCursorLine);
|
|
547
|
+
if (!isCursorLine) {
|
|
548
|
+
return prefix + this.textStyle.inline(true).render(line);
|
|
549
|
+
}
|
|
550
|
+
const graphemes = splitter.splitGraphemes(line ?? "");
|
|
551
|
+
const col = clamp(this.pos.column, 0, graphemes.length);
|
|
552
|
+
const before = graphemes.slice(0, col).join("");
|
|
553
|
+
const after = graphemes.slice(col).join("");
|
|
554
|
+
const cursorChar = "\u258C";
|
|
555
|
+
const beforeStyled = this.textStyle.inline(true).render(before);
|
|
556
|
+
const afterStyled = this.textStyle.inline(true).render(after);
|
|
557
|
+
const cursorView = this.cursor.withChar(cursorChar).view();
|
|
558
|
+
return prefix + beforeStyled + cursorView + afterStyled;
|
|
559
|
+
}
|
|
560
|
+
#linePrefix(lineIndex, _isCursorLine) {
|
|
561
|
+
const prompt = this.prompt ? this.promptStyle.inline(true).render(this.prompt) : "";
|
|
562
|
+
const ln = this.showLineNumbers && this.lines.length > 0 ? this.#renderLineNumber(lineIndex) : "";
|
|
563
|
+
return ln + prompt;
|
|
564
|
+
}
|
|
565
|
+
#renderLineNumber(lineIndex) {
|
|
566
|
+
const width = String(this.lines.length).length;
|
|
567
|
+
const num = String(lineIndex + 1).padStart(width, " ");
|
|
568
|
+
return this.lineNumberStyle.inline(true).render(`${num} `);
|
|
569
|
+
}
|
|
570
|
+
#runValidation(value) {
|
|
571
|
+
return this.validateFn ? this.validateFn(value) : null;
|
|
572
|
+
}
|
|
573
|
+
#withError(error) {
|
|
574
|
+
if (error === this.error) return this;
|
|
575
|
+
return this.#with({ error });
|
|
576
|
+
}
|
|
577
|
+
#withValue(lines, pos) {
|
|
578
|
+
const normalized = lines.length > 0 ? lines : [""];
|
|
579
|
+
const nextPos = this.#clampPosition(pos, normalized);
|
|
580
|
+
const next = this.#with({
|
|
581
|
+
lines: normalized,
|
|
582
|
+
pos: nextPos
|
|
583
|
+
});
|
|
584
|
+
return next.#withError(next.#runValidation(next.value())).#ensureCursorVisible();
|
|
585
|
+
}
|
|
586
|
+
#withPosition(pos) {
|
|
587
|
+
return this.#withValue(this.lines, pos);
|
|
588
|
+
}
|
|
589
|
+
#with(patch) {
|
|
590
|
+
return new _TextareaModel({
|
|
591
|
+
lines: this.lines,
|
|
592
|
+
pos: this.pos,
|
|
593
|
+
focused: this.focused,
|
|
594
|
+
width: this.width,
|
|
595
|
+
maxHeight: this.maxHeight,
|
|
596
|
+
maxWidth: this.maxWidth,
|
|
597
|
+
prompt: this.prompt,
|
|
598
|
+
promptStyle: this.promptStyle,
|
|
599
|
+
textStyle: this.textStyle,
|
|
600
|
+
placeholderStyle: this.placeholderStyle,
|
|
601
|
+
cursorStyle: this.cursorStyle,
|
|
602
|
+
lineNumberStyle: this.lineNumberStyle,
|
|
603
|
+
showLineNumbers: this.showLineNumbers,
|
|
604
|
+
endOfBufferCharacter: this.endOfBufferCharacter,
|
|
605
|
+
validateFn: this.validateFn,
|
|
606
|
+
keyMap: this.keyMap,
|
|
607
|
+
cursor: this.cursor,
|
|
608
|
+
error: this.error,
|
|
609
|
+
scrollTop: this.scrollTop,
|
|
610
|
+
placeholder: this.placeholder,
|
|
611
|
+
...patch
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
#clampPosition(pos, lines) {
|
|
615
|
+
const line = clamp(pos.line, 0, Math.max(0, lines.length - 1));
|
|
616
|
+
const lineContent = lines[line] ?? "";
|
|
617
|
+
const column = clamp(pos.column, 0, runeCount(lineContent));
|
|
618
|
+
return { line, column };
|
|
619
|
+
}
|
|
620
|
+
#ensureCursorVisible() {
|
|
621
|
+
const height = this.viewportHeight();
|
|
622
|
+
if (height <= 0) return this;
|
|
623
|
+
let top = this.scrollTop;
|
|
624
|
+
if (this.pos.line < top) {
|
|
625
|
+
top = this.pos.line;
|
|
626
|
+
} else if (this.pos.line >= top + height) {
|
|
627
|
+
top = this.pos.line - height + 1;
|
|
628
|
+
}
|
|
629
|
+
top = clamp(top, 0, this.maxScroll());
|
|
630
|
+
if (top === this.scrollTop) return this;
|
|
631
|
+
return this.#with({ scrollTop: top });
|
|
632
|
+
}
|
|
633
|
+
viewportHeight() {
|
|
634
|
+
if (this.maxHeight && this.maxHeight > 0) {
|
|
635
|
+
return this.maxHeight;
|
|
636
|
+
}
|
|
637
|
+
return Math.max(1, this.lines.length);
|
|
638
|
+
}
|
|
639
|
+
maxScroll() {
|
|
640
|
+
const height = this.viewportHeight();
|
|
641
|
+
return Math.max(0, this.lines.length - height);
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
function sanitize(value) {
|
|
645
|
+
return sanitizer.sanitize(value ?? "");
|
|
646
|
+
}
|
|
647
|
+
function splitLines(value) {
|
|
648
|
+
return value.split("\n");
|
|
649
|
+
}
|
|
650
|
+
function sliceGraphemes(value, start, end) {
|
|
651
|
+
const graphemes = splitter.splitGraphemes(value ?? "");
|
|
652
|
+
return graphemes.slice(start, end).join("");
|
|
653
|
+
}
|
|
654
|
+
function runeCount(value) {
|
|
655
|
+
return splitter.countGraphemes(value ?? "");
|
|
656
|
+
}
|
|
657
|
+
function clamp(v, min, max) {
|
|
658
|
+
const lo = Math.min(min, max);
|
|
659
|
+
const hi = Math.max(min, max);
|
|
660
|
+
return Math.min(hi, Math.max(lo, v));
|
|
661
|
+
}
|
|
662
|
+
function last(arr) {
|
|
663
|
+
return arr[arr.length - 1];
|
|
664
|
+
}
|
|
665
|
+
function replaceLine(lines, index, value) {
|
|
666
|
+
return lines.map((l, i) => i === index ? value : l);
|
|
667
|
+
}
|
|
668
|
+
function asError(err) {
|
|
669
|
+
if (err instanceof Error) return err;
|
|
670
|
+
if (typeof err === "string") return new Error(err);
|
|
671
|
+
if (typeof err === "number" || typeof err === "boolean")
|
|
672
|
+
return new Error(String(err));
|
|
673
|
+
return new Error("textarea error");
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
Object.defineProperty(exports, "CursorMode", {
|
|
677
|
+
enumerable: true,
|
|
678
|
+
get: function () { return cursor.CursorMode; }
|
|
679
|
+
});
|
|
680
|
+
exports.PasteErrorMsg = PasteErrorMsg;
|
|
681
|
+
exports.PasteMsg = PasteMsg;
|
|
682
|
+
exports.TextareaModel = TextareaModel;
|
|
683
|
+
exports.defaultKeyMap = defaultKeyMap;
|
|
684
|
+
exports.pasteCommand = pasteCommand;
|
|
685
|
+
//# sourceMappingURL=index.cjs.map
|
|
686
|
+
//# sourceMappingURL=index.cjs.map
|