blots-editor 0.0.10 → 0.0.11

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.
@@ -163,8 +163,19 @@ export class Document {
163
163
  /** Set the cursor, clamping out-of-range values (line to `[0, lineCount-1]`, column to end of line). */
164
164
  setCursor(line, logical) {
165
165
  const l = Math.max(0, Math.min(line, this._lines.length - 1));
166
- const len = this._lines[l].text.length;
167
- this._cursor = { line: l, logical: Math.max(0, Math.min(logical, len)) };
166
+ const text = this._lines[l].text;
167
+ let col = Math.max(0, Math.min(logical, text.length));
168
+ // The cursor must never sit between the halves of a surrogate pair —
169
+ // typing there would split the emoji — so snap it to the pair start.
170
+ if (col > 0 &&
171
+ col < text.length &&
172
+ text.charCodeAt(col) >= 0xdc00 &&
173
+ text.charCodeAt(col) <= 0xdfff &&
174
+ text.charCodeAt(col - 1) >= 0xd800 &&
175
+ text.charCodeAt(col - 1) <= 0xdbff) {
176
+ col--;
177
+ }
178
+ this._cursor = { line: l, logical: col };
168
179
  // Any cursor movement releases the manual view-scroll lock so the
169
180
  // viewport follows the cursor again.
170
181
  this._viewScrollTop = null;
@@ -189,13 +200,28 @@ export class Document {
189
200
  moveLeft() {
190
201
  const { line, logical } = this._cursor;
191
202
  if (logical > 0) {
192
- this.setCursor(line, logical - 1);
203
+ let target = logical - 1;
204
+ // Step over the whole emoji: stopping at its trailing half would
205
+ // leave the cursor inside the pair.
206
+ const ch = this._lines[line].text.charCodeAt(target);
207
+ if (target > 0 && ch >= 0xdc00 && ch <= 0xdfff) {
208
+ target--;
209
+ }
210
+ this.setCursor(line, target);
193
211
  }
194
212
  }
195
213
  moveRight() {
196
214
  const { line, logical } = this._cursor;
197
- if (logical < this._lines[line].text.length) {
198
- this.setCursor(line, logical + 1);
215
+ const text = this._lines[line].text;
216
+ if (logical < text.length) {
217
+ let target = logical + 1;
218
+ // Step over the whole emoji: stopping at its leading half would
219
+ // leave the cursor inside the pair.
220
+ const ch = text.charCodeAt(logical);
221
+ if (target < text.length && ch >= 0xd800 && ch <= 0xdbff) {
222
+ target++;
223
+ }
224
+ this.setCursor(line, target);
199
225
  }
200
226
  }
201
227
  /** Vertical movement aligns by visual column, so wide-character lines do not drift. */
@@ -26,10 +26,15 @@ export class DeleteBeforeOp {
26
26
  const { line, logical } = doc.cursor;
27
27
  if (logical > 0) {
28
28
  const cur = doc.getLine(line);
29
- this._deletedChar = cur[logical - 1];
29
+ // Deleting only the trailing half of a surrogate pair would leave a
30
+ // broken half-rendering char, so delete the whole code point.
31
+ const last = cur.charCodeAt(logical - 1);
32
+ const isTrail = last >= 0xdc00 && last <= 0xdfff && logical >= 2;
33
+ const units = isTrail ? 2 : 1;
34
+ this._deletedChar = cur.slice(logical - units, logical);
30
35
  this._joined = false;
31
- doc.setLine(line, cur.slice(0, logical - 1) + cur.slice(logical));
32
- doc.setCursor(line, logical - 1);
36
+ doc.setLine(line, cur.slice(0, logical - units) + cur.slice(logical));
37
+ doc.setCursor(line, logical - units);
33
38
  }
34
39
  else if (line > 0) {
35
40
  const prev = doc.getLine(line - 1);
@@ -45,7 +50,7 @@ export class DeleteBeforeOp {
45
50
  const { line, logical } = doc.cursor;
46
51
  const cur = doc.getLine(line);
47
52
  doc.setLine(line, cur.slice(0, logical) + this._deletedChar + cur.slice(logical));
48
- doc.setCursor(line, logical + 1);
53
+ doc.setCursor(line, logical + this._deletedChar.length);
49
54
  }
50
55
  else {
51
56
  // Cursor sits at the end of the merged line; split it back at that point.
@@ -65,9 +70,14 @@ export class DeleteAfterOp {
65
70
  const { line, logical } = doc.cursor;
66
71
  const cur = doc.getLine(line);
67
72
  if (logical < cur.length) {
68
- this._deletedChar = cur[logical];
73
+ // Deleting only the leading half of a surrogate pair would leave a
74
+ // broken half-rendering char, so delete the whole code point.
75
+ const first = cur.charCodeAt(logical);
76
+ const isLead = first >= 0xd800 && first <= 0xdbff && logical + 1 < cur.length;
77
+ const units = isLead ? 2 : 1;
78
+ this._deletedChar = cur.slice(logical, logical + units);
69
79
  this._joined = false;
70
- doc.setLine(line, cur.slice(0, logical) + cur.slice(logical + 1));
80
+ doc.setLine(line, cur.slice(0, logical) + cur.slice(logical + units));
71
81
  }
72
82
  else if (line < doc.lineCount - 1) {
73
83
  const next = doc.getLine(line + 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blots-editor",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "description": "A lightweight Markdown editor for the terminal, built on ink-cartridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",