@flowtty/core 1.0.0-alpha.7 → 1.0.0-alpha.8

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/dist/index.d.ts CHANGED
@@ -82,6 +82,10 @@ declare function stringWidth(str: string): number;
82
82
 
83
83
  interface EditorState {
84
84
  value: string;
85
+ /** UTF-16 index into `value` (so `value.slice(0, cursor)` is the text before
86
+ * the caret). It only ever rests on a character boundary: movement and
87
+ * deletion step over a whole code point, and a cursor handed in between the
88
+ * two halves of a surrogate pair is snapped back before anything else. */
85
89
  cursor: number;
86
90
  }
87
91
  type EditorAction = {
@@ -115,6 +119,8 @@ interface InputRow {
115
119
  continuation: boolean;
116
120
  }
117
121
  declare function inputRows(value: string, width: number, cursor?: number): InputRow[];
122
+ /** UTF-16 index of character column `col` within `row` (clamped to its end). */
123
+ declare function rowIndexAt(row: InputRow, col: number): number;
118
124
  declare function caretPosition(value: string, cursor: number, width: number): {
119
125
  row: number;
120
126
  col: number;
@@ -164,4 +170,4 @@ type MultiSelectAction = {
164
170
  };
165
171
  declare function reduce<T>(items: SelectItem<T>[], state: MultiSelectState, key: Key): MultiSelectAction;
166
172
 
167
- export { type EditorAction, type EditorOptions, type EditorState, type InputRow, Key, type MultiSelectAction, type MultiSelectState, type SelectAction, type SelectItem, type SelectState, type VisualLine, type WrapMode, caretPosition, charWidth, reduce$2 as editorReducer, inputRows, reduce as multiSelectReducer, reduce$1 as selectReducer, splitVisualLines, stringWidth, visibleIndices, windowAround, wrapText };
173
+ export { type EditorAction, type EditorOptions, type EditorState, type InputRow, Key, type MultiSelectAction, type MultiSelectState, type SelectAction, type SelectItem, type SelectState, type VisualLine, type WrapMode, caretPosition, charWidth, reduce$2 as editorReducer, inputRows, reduce as multiSelectReducer, rowIndexAt, reduce$1 as selectReducer, splitVisualLines, stringWidth, visibleIndices, windowAround, wrapText };
package/dist/index.js CHANGED
@@ -287,13 +287,17 @@ function inputRows(value, width, cursor) {
287
287
  const rows = [];
288
288
  let lineStart = 0;
289
289
  for (const line of value.split("\n")) {
290
- if (line.length === 0) {
290
+ const chars = [...line];
291
+ if (chars.length === 0) {
291
292
  rows.push({ text: "", start: lineStart, continuation: false });
292
293
  } else {
293
- for (let at = 0; at < line.length; at += w) {
294
- rows.push({ text: line.slice(at, at + w), start: lineStart + at, continuation: at > 0 });
294
+ let start = lineStart;
295
+ for (let at = 0; at < chars.length; at += w) {
296
+ const text = chars.slice(at, at + w).join("");
297
+ rows.push({ text, start, continuation: at > 0 });
298
+ start += text.length;
295
299
  }
296
- if (cursor === lineStart + line.length && line.length % w === 0) {
300
+ if (cursor === lineStart + line.length && chars.length % w === 0) {
297
301
  rows.push({ text: "", start: cursor, continuation: true });
298
302
  }
299
303
  }
@@ -301,17 +305,26 @@ function inputRows(value, width, cursor) {
301
305
  }
302
306
  return rows;
303
307
  }
308
+ function rowIndexAt(row, col) {
309
+ return row.start + [...row.text].slice(0, Math.max(0, col)).join("").length;
310
+ }
304
311
  function caretPosition(value, cursor, width) {
305
312
  const c = Math.max(0, Math.min(value.length, cursor));
306
313
  const rows = inputRows(value, width, c);
307
314
  for (let r = rows.length - 1; r >= 0; r--) {
308
315
  const row = rows[r];
309
- if (row.start <= c && c <= row.start + row.text.length) return { row: r, col: c - row.start };
316
+ if (row.start <= c && c <= row.start + row.text.length) {
317
+ return { row: r, col: [...value.slice(row.start, c)].length };
318
+ }
310
319
  }
311
320
  return { row: 0, col: 0 };
312
321
  }
313
322
 
314
323
  // src/editor.ts
324
+ var isHigh = (u) => u !== void 0 && u >= "\uD800" && u <= "\uDBFF";
325
+ var isLow = (u) => u !== void 0 && u >= "\uDC00" && u <= "\uDFFF";
326
+ var prevIndex = (v, i) => i >= 2 && isLow(v[i - 1]) && isHigh(v[i - 2]) ? i - 2 : Math.max(0, i - 1);
327
+ var nextIndex = (v, i) => isHigh(v[i]) && isLow(v[i + 1]) ? i + 2 : Math.min(v.length, i + 1);
315
328
  var OPT_MAP = {
316
329
  " ": ["\xA0"],
317
330
  // U+00A0 NBSP
@@ -336,8 +349,9 @@ function wordRight(value, cursor) {
336
349
  return c;
337
350
  }
338
351
  function reduce(state, key, opts = {}) {
339
- const { value, cursor } = state;
340
- const clamp = (n) => Math.max(0, Math.min(value.length, n));
352
+ const { value } = state;
353
+ let cursor = Math.max(0, Math.min(value.length, state.cursor));
354
+ if (isLow(value[cursor]) && isHigh(value[cursor - 1])) cursor--;
341
355
  const multiline = opts.multiline === true;
342
356
  const lineStart = multiline ? value.lastIndexOf("\n", cursor - 1) + 1 : 0;
343
357
  const nextBreak = multiline ? value.indexOf("\n", cursor) : -1;
@@ -347,7 +361,8 @@ function reduce(state, key, opts = {}) {
347
361
  state: { value: value.slice(0, cursor) + text + value.slice(cursor), cursor: cursor + text.length }
348
362
  });
349
363
  if (key.name === "paste") {
350
- const text = multiline ? key.text ?? "" : (key.text ?? "").replace(/\n/g, " ");
364
+ const pasted = (key.text ?? "").replace(/\r\n?/g, "\n");
365
+ const text = multiline ? pasted : pasted.replace(/\n/g, " ");
351
366
  if (text === "") return { kind: "noop" };
352
367
  return insert(text);
353
368
  }
@@ -364,15 +379,15 @@ function reduce(state, key, opts = {}) {
364
379
  if (target < 0) return { kind: "edit", state: { value, cursor: 0 } };
365
380
  if (target >= rows.length) return { kind: "edit", state: { value, cursor: value.length } };
366
381
  const row = rows[target];
367
- return { kind: "edit", state: { value, cursor: row.start + Math.min(at.col, row.text.length) } };
382
+ return { kind: "edit", state: { value, cursor: rowIndexAt(row, at.col) } };
368
383
  }
369
384
  }
370
385
  if (key.name === "left" && key.meta) return { kind: "edit", state: { value, cursor: wordLeft(value, cursor) } };
371
386
  if (key.name === "b" && key.meta) return { kind: "edit", state: { value, cursor: wordLeft(value, cursor) } };
372
387
  if (key.name === "right" && key.meta) return { kind: "edit", state: { value, cursor: wordRight(value, cursor) } };
373
388
  if (key.name === "f" && key.meta) return { kind: "edit", state: { value, cursor: wordRight(value, cursor) } };
374
- if (key.name === "left") return { kind: "edit", state: { value, cursor: clamp(cursor - 1) } };
375
- if (key.name === "right") return { kind: "edit", state: { value, cursor: clamp(cursor + 1) } };
389
+ if (key.name === "left") return { kind: "edit", state: { value, cursor: prevIndex(value, cursor) } };
390
+ if (key.name === "right") return { kind: "edit", state: { value, cursor: nextIndex(value, cursor) } };
376
391
  if (key.name === "home") return { kind: "edit", state: { value, cursor: lineStart } };
377
392
  if (key.name === "end") return { kind: "edit", state: { value, cursor: lineEnd } };
378
393
  if (key.name === "a" && key.ctrl) return { kind: "edit", state: { value, cursor: lineStart } };
@@ -405,19 +420,17 @@ function reduce(state, key, opts = {}) {
405
420
  return { kind: "edit", state: { value: value.slice(0, cursor) + ch + value.slice(cursor), cursor: cursor + 1 } };
406
421
  }
407
422
  if (key.name === "backspace") {
408
- if (cursor === 0) return { kind: "edit", state };
409
- return { kind: "edit", state: { value: value.slice(0, cursor - 1) + value.slice(cursor), cursor: cursor - 1 } };
423
+ if (cursor === 0) return { kind: "edit", state: { value, cursor } };
424
+ const from = prevIndex(value, cursor);
425
+ return { kind: "edit", state: { value: value.slice(0, from) + value.slice(cursor), cursor: from } };
410
426
  }
411
427
  if (key.name === "delete" || key.name === "d" && key.ctrl) {
412
- if (cursor === value.length) return { kind: "edit", state };
413
- return { kind: "edit", state: { value: value.slice(0, cursor) + value.slice(cursor + 1), cursor } };
428
+ if (cursor === value.length) return { kind: "edit", state: { value, cursor } };
429
+ return { kind: "edit", state: { value: value.slice(0, cursor) + value.slice(nextIndex(value, cursor)), cursor } };
414
430
  }
415
431
  if (key.name === "return") return { kind: "submit" };
416
432
  if (key.name === "escape") return { kind: "cancel" };
417
- if (!key.ctrl && !key.meta && key.name.length === 1) {
418
- const ch = key.name;
419
- return { kind: "edit", state: { value: value.slice(0, cursor) + ch + value.slice(cursor), cursor: cursor + 1 } };
420
- }
433
+ if (!key.ctrl && !key.meta && [...key.name].length === 1) return insert(key.name);
421
434
  return { kind: "noop" };
422
435
  }
423
436
 
@@ -477,4 +490,4 @@ function reduce3(items, state, key) {
477
490
  return { kind: "noop" };
478
491
  }
479
492
 
480
- export { caretPosition, charWidth, reduce as editorReducer, inputRows, reduce3 as multiSelectReducer, reduce2 as selectReducer, splitVisualLines, stringWidth, visibleIndices, windowAround };
493
+ export { caretPosition, charWidth, reduce as editorReducer, inputRows, reduce3 as multiSelectReducer, rowIndexAt, reduce2 as selectReducer, splitVisualLines, stringWidth, visibleIndices, windowAround };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowtty/core",
3
- "version": "1.0.0-alpha.7",
3
+ "version": "1.0.0-alpha.8",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {