reslop 0.2.5 → 0.2.7

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.
Files changed (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +72 -19
  6. package/lib/api.js +1 -1
  7. package/lib/capabilities.js +8 -8
  8. package/lib/cli.js +61 -75
  9. package/lib/clipboard.js +39 -47
  10. package/lib/dashboard/git.js +128 -0
  11. package/lib/dashboard/model.js +357 -0
  12. package/lib/dashboard/npm.js +290 -0
  13. package/lib/dashboard/tree.js +278 -0
  14. package/lib/deps-diff.js +197 -402
  15. package/lib/deps.js +113 -240
  16. package/lib/detect.js +8 -18
  17. package/lib/diff/align.js +41 -113
  18. package/lib/diff/diff.js +12 -43
  19. package/lib/diff/display.js +25 -36
  20. package/lib/diff/edit.js +6 -20
  21. package/lib/diff/inline.js +76 -144
  22. package/lib/diff/items.js +33 -47
  23. package/lib/diff/parse.js +28 -56
  24. package/lib/diff/patch.js +12 -33
  25. package/lib/diff/unit.js +79 -178
  26. package/lib/editor.js +52 -69
  27. package/lib/files.js +79 -91
  28. package/lib/find.js +61 -0
  29. package/lib/git-branches.js +254 -416
  30. package/lib/git-deps.js +148 -272
  31. package/lib/git-reword-todo.js +5 -13
  32. package/lib/git-worktree.js +161 -214
  33. package/lib/git.js +21 -49
  34. package/lib/github.js +67 -181
  35. package/lib/gitlab.js +40 -90
  36. package/lib/highlight/bash.js +55 -177
  37. package/lib/highlight/core.js +155 -141
  38. package/lib/highlight/css.js +25 -58
  39. package/lib/highlight/csv.js +19 -37
  40. package/lib/highlight/dart.js +72 -213
  41. package/lib/highlight/dot.js +14 -14
  42. package/lib/highlight/html.js +42 -63
  43. package/lib/highlight/js.js +177 -506
  44. package/lib/highlight/json.js +17 -37
  45. package/lib/highlight/log.js +19 -25
  46. package/lib/highlight/txt.js +7 -10
  47. package/lib/highlight.js +17 -46
  48. package/lib/keys.js +75 -83
  49. package/lib/manifest.js +67 -126
  50. package/lib/npm-commands.js +120 -169
  51. package/lib/remote.js +140 -190
  52. package/lib/render/branches.js +63 -61
  53. package/lib/render/chrome.js +412 -347
  54. package/lib/render/commits.js +160 -361
  55. package/lib/render/dash-activity.js +212 -0
  56. package/lib/render/dash-blocks.js +496 -0
  57. package/lib/render/dash-table.js +295 -0
  58. package/lib/render/dashboard.js +62 -0
  59. package/lib/render/diff.js +123 -258
  60. package/lib/render/files.js +60 -86
  61. package/lib/render/frame.js +82 -117
  62. package/lib/render/notes.js +150 -210
  63. package/lib/render/npm.js +213 -177
  64. package/lib/render/packages.js +93 -0
  65. package/lib/render/present.js +4 -11
  66. package/lib/render/primitives.js +77 -172
  67. package/lib/render/render.js +5 -14
  68. package/lib/render/report.js +43 -0
  69. package/lib/render/repos.js +226 -0
  70. package/lib/render/search.js +25 -0
  71. package/lib/render/tiles.js +355 -0
  72. package/lib/render/unit.js +15 -31
  73. package/lib/report-model.js +110 -33
  74. package/lib/report-parse.js +56 -37
  75. package/lib/report-render.js +102 -59
  76. package/lib/report-run.js +87 -77
  77. package/lib/report-test.js +221 -99
  78. package/lib/report-tools.js +96 -154
  79. package/lib/review-merge.js +33 -41
  80. package/lib/review.js +69 -129
  81. package/lib/runs.js +225 -0
  82. package/lib/select.js +53 -91
  83. package/lib/session/accessors.js +14 -21
  84. package/lib/session/actions.js +189 -65
  85. package/lib/session/branches.js +164 -222
  86. package/lib/session/change-actions.js +49 -119
  87. package/lib/session/commit.js +298 -352
  88. package/lib/session/compose.js +572 -715
  89. package/lib/session/dashboard.js +530 -0
  90. package/lib/session/dispatch.js +197 -171
  91. package/lib/session/files-pane.js +92 -45
  92. package/lib/session/find.js +94 -0
  93. package/lib/session/items.js +6 -17
  94. package/lib/session/lifecycle.js +420 -466
  95. package/lib/session/load.js +1 -14
  96. package/lib/session/navigation.js +4 -2
  97. package/lib/session/npm.js +403 -420
  98. package/lib/session/ops.js +5 -13
  99. package/lib/session/packages.js +143 -0
  100. package/lib/session/pointer.js +30 -14
  101. package/lib/session/progress.js +39 -30
  102. package/lib/session/review.js +1 -3
  103. package/lib/session/tasks.js +277 -236
  104. package/lib/session/templates.js +51 -59
  105. package/lib/session/terminal.js +85 -132
  106. package/lib/session/view.js +59 -80
  107. package/lib/session/watch.js +201 -212
  108. package/lib/session/workspace.js +455 -0
  109. package/lib/session.js +95 -161
  110. package/lib/update.js +44 -111
  111. package/lib/utilities.js +46 -88
  112. package/lib/workspace.js +166 -0
  113. package/lib/wrap.js +29 -55
  114. package/package.json +1 -1
package/lib/diff/unit.js CHANGED
@@ -5,16 +5,14 @@ const { attachInline } = align;
5
5
  const files = require('../files.js');
6
6
  const { listPath } = files;
7
7
 
8
- const splitEditor = (text) => {
9
- const lines = `${text}`.split('\n');
10
- if (!lines.length) return [''];
11
- return lines;
12
- };
8
+ const LCS_CELLS = 4000000;
9
+
10
+ const splitEditor = (text) => text.split('\n');
13
11
 
14
12
  const splitFile = (text) => {
15
13
  if (!text) return [];
16
- const lines = `${text}`.split('\n');
17
- if (lines.length && lines[lines.length - 1] === '') lines.pop();
14
+ const lines = text.split('\n');
15
+ if (lines[lines.length - 1] === '') lines.pop();
18
16
  return lines;
19
17
  };
20
18
 
@@ -28,53 +26,13 @@ const hunkKey = (item) => {
28
26
 
29
27
  const sameHunk = (left, right) => {
30
28
  if (!left || !right) return false;
31
- if (left.hunk && right.hunk && left.hunk === right.hunk) {
29
+ if (left.hunk && left.hunk === right.hunk) {
32
30
  return listPath(left) === listPath(right);
33
31
  }
34
- const key = hunkKey(left);
35
- if (!key) return false;
36
- return key === hunkKey(right);
37
- };
38
-
39
- const emptyMarks = (last) => {
40
- const size = last + 1;
41
- const dels = new Array(size);
42
- const adds = new Array(size);
43
- for (let i = 0; i < size; i++) {
44
- dels[i] = [];
45
- adds[i] = null;
46
- }
47
- return { dels, adds };
48
- };
49
-
50
- const clipAt = (n, last) => Math.min(Math.max(1, n), last);
51
-
52
- const applyHunk = (marks, item) => {
53
- const hunk = item.hunk;
54
- if (!hunk || !hunk.lines) return;
55
- const last = marks.dels.length - 1;
56
- let newLine = hunk.newStart > 0 ? hunk.newStart : 1;
57
- for (const line of hunk.lines) {
58
- const extra = {
59
- text: line.text ?? '',
60
- item,
61
- blockId: line.blockId,
62
- origin: item.origin,
63
- };
64
- if (line.type === 'del') {
65
- marks.dels[clipAt(newLine, last)].push(extra);
66
- continue;
67
- }
68
- if (line.type === 'add') {
69
- marks.adds[clipAt(newLine, last)] = extra;
70
- newLine += 1;
71
- continue;
72
- }
73
- newLine += 1;
74
- }
32
+ return hunkKey(left) === hunkKey(right);
75
33
  };
76
34
 
77
- const tagged = (type, text, extra = {}) => ({
35
+ const tagged = (type, text, extra) => ({
78
36
  type,
79
37
  text,
80
38
  noNl: false,
@@ -83,20 +41,35 @@ const tagged = (type, text, extra = {}) => ({
83
41
  item: extra.item ?? null,
84
42
  });
85
43
 
86
- const itemsForPath = (items, rel) => {
87
- const out = [];
88
- for (const item of items) {
89
- if (listPath(item) === rel) out.push(item);
44
+ const NO_EXTRA = {};
45
+
46
+ const clipAt = (n, last) => Math.min(Math.max(1, n), last);
47
+
48
+ const markHunk = (marks, item) => {
49
+ if (!item.hunk) return;
50
+ const last = marks.dels.length - 1;
51
+ let newLine = item.hunk.newStart > 0 ? item.hunk.newStart : 1;
52
+ for (const line of item.hunk.lines) {
53
+ const { type, text, blockId } = line;
54
+ const mark = { text, item, blockId, origin: item.origin };
55
+ if (type === 'del') {
56
+ marks.dels[clipAt(newLine, last)].push(mark);
57
+ continue;
58
+ }
59
+ if (type === 'add') marks.adds[clipAt(newLine, last)] = mark;
60
+ newLine += 1;
90
61
  }
91
- return out;
92
62
  };
93
63
 
64
+ const itemsForPath = (items, rel) =>
65
+ items.filter((item) => listPath(item) === rel);
66
+
94
67
  const rowsFromAdds = (items) => {
95
68
  const rows = [];
96
69
  for (const item of items) {
97
- const lines = item.hunk && item.hunk.lines ? item.hunk.lines : [];
98
- for (const line of lines) {
99
- if (line.type === 'add') rows.push(line.text ?? '');
70
+ if (!item.hunk) continue;
71
+ for (const line of item.hunk.lines) {
72
+ if (line.type === 'add') rows.push(line.text);
100
73
  }
101
74
  }
102
75
  return rows;
@@ -105,28 +78,28 @@ const rowsFromAdds = (items) => {
105
78
  const unitLines = (text, items) => {
106
79
  let rows = splitFile(text);
107
80
  if (!rows.length) rows = rowsFromAdds(items);
108
- const marks = emptyMarks(rows.length + 1);
109
- for (const item of items) applyHunk(marks, item);
81
+ const size = rows.length + 2;
82
+ const marks = {
83
+ dels: Array.from({ length: size }, () => []),
84
+ adds: new Array(size).fill(null),
85
+ };
86
+ for (const item of items) markHunk(marks, item);
110
87
  const out = [];
111
88
  for (let i = 0; i < rows.length; i++) {
112
- const n = i + 1;
113
- for (const del of marks.dels[n]) out.push(tagged('del', del.text, del));
114
- const add = marks.adds[n];
89
+ for (const del of marks.dels[i + 1]) out.push(tagged('del', del.text, del));
90
+ const add = marks.adds[i + 1];
115
91
  if (add) out.push(tagged('add', rows[i], add));
116
- else out.push(tagged('ctx', rows[i], {}));
92
+ else out.push(tagged('ctx', rows[i], NO_EXTRA));
117
93
  }
118
94
  for (const del of marks.dels[rows.length + 1]) {
119
95
  out.push(tagged('del', del.text, del));
120
96
  }
121
- if (!out.length) out.push(tagged('ctx', '', {}));
97
+ if (!out.length) out.push(tagged('ctx', '', NO_EXTRA));
122
98
  return attachInline(out);
123
99
  };
124
100
 
125
- const sameBlock = (line, item) => {
126
- if (!line || !item) return false;
127
- if (line.item !== item) return false;
128
- return line.blockId === item.blockId;
129
- };
101
+ const sameBlock = (line, item) =>
102
+ line.item === item && line.blockId === item.blockId;
130
103
 
131
104
  const changedLineRange = (lines, item, belongs) => {
132
105
  let start = -1;
@@ -151,12 +124,11 @@ const hunkLineRange = (lines, item) =>
151
124
 
152
125
  const fileOffset = (lines, at) => {
153
126
  let offset = 0;
154
- const target = Math.max(0, at);
155
127
  for (let i = 0; i < lines.length; i++) {
156
128
  const line = lines[i];
157
129
  if (line.type === 'del') continue;
158
- if (i >= target) return offset;
159
- offset += (line.text ?? '').length + 1;
130
+ if (i >= at) return offset;
131
+ offset += line.text.length + 1;
160
132
  }
161
133
  return offset;
162
134
  };
@@ -173,37 +145,27 @@ const fileLineIndex = (lines, fileLine) => {
173
145
  return last;
174
146
  };
175
147
 
176
- const lastEditableIndex = (lines) => {
177
- for (let i = lines.length - 1; i >= 0; i--) {
178
- if (lines[i].type !== 'del') return i;
179
- }
180
- return -1;
181
- };
182
-
183
148
  const attachEditSpans = (lines) => {
149
+ let lastAt = lines.length - 1;
150
+ while (lastAt >= 0 && lines[lastAt].type === 'del') lastAt -= 1;
184
151
  let offset = 0;
185
- const lastAt = lastEditableIndex(lines);
186
152
  for (let i = 0; i < lines.length; i++) {
187
153
  const line = lines[i];
188
154
  if (line.type === 'del') continue;
189
- const text = line.text ?? '';
190
- const last = i === lastAt;
191
155
  line.wrap = true;
192
156
  line.editStart = offset;
193
- line.editEnd = offset + text.length;
194
- line.editLast = last;
195
- offset = last ? offset + text.length : offset + text.length + 1;
157
+ line.editEnd = offset + line.text.length;
158
+ line.editLast = i === lastAt;
159
+ offset = line.editEnd + 1;
196
160
  }
197
161
  return lines;
198
162
  };
199
163
 
200
- const LCS_CELLS = 4000000;
201
-
202
164
  const lcsTable = (prev, next) => {
203
165
  const n = prev.length;
204
166
  const m = next.length;
205
167
  const dp = new Array(n + 1);
206
- for (let i = 0; i <= n; i++) dp[i] = new Array(m + 1).fill(0);
168
+ for (let i = 0; i <= n; i++) dp[i] = new Uint32Array(m + 1);
207
169
  for (let i = n - 1; i >= 0; i--) {
208
170
  for (let j = m - 1; j >= 0; j--) {
209
171
  if (prev[i] === next[j]) dp[i][j] = dp[i + 1][j + 1] + 1;
@@ -215,44 +177,35 @@ const lcsTable = (prev, next) => {
215
177
 
216
178
  const zipOps = (prev, next) => {
217
179
  const n = Math.min(prev.length, next.length);
218
- const ops = new Array(Math.max(prev.length, next.length));
219
- let at = 0;
220
- for (let i = 0; i < n; i++) ops[at++] = { kind: 'rep', text: next[i] };
221
- for (let i = n; i < prev.length; i++) ops[at++] = { kind: 'del' };
180
+ const ops = [];
181
+ for (let i = 0; i < n; i++) ops.push({ kind: 'rep', text: next[i] });
182
+ for (let i = n; i < prev.length; i++) ops.push({ kind: 'del' });
222
183
  for (let i = n; i < next.length; i++) {
223
- ops[at++] = { kind: 'ins', text: next[i] };
184
+ ops.push({ kind: 'ins', text: next[i] });
224
185
  }
225
186
  return ops;
226
187
  };
227
188
 
228
- const walkLcs = (prev, next, dp) => {
229
- const ops = new Array(prev.length + next.length - dp[0][0]);
230
- let n = 0;
189
+ const lcsOps = (prev, next) => {
190
+ const dp = lcsTable(prev, next);
191
+ const ops = [];
231
192
  let i = 0;
232
193
  let j = 0;
233
194
  while (i < prev.length && j < next.length) {
234
195
  if (prev[i] === next[j]) {
235
- ops[n++] = { kind: 'eq', text: next[j] };
196
+ ops.push({ kind: 'eq', text: next[j] });
236
197
  i += 1;
237
198
  j += 1;
238
- continue;
239
- }
240
- if (dp[i + 1][j] >= dp[i][j + 1]) {
241
- ops[n++] = { kind: 'del' };
199
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
200
+ ops.push({ kind: 'del' });
242
201
  i += 1;
243
- continue;
202
+ } else {
203
+ ops.push({ kind: 'ins', text: next[j] });
204
+ j += 1;
244
205
  }
245
- ops[n++] = { kind: 'ins', text: next[j] };
246
- j += 1;
247
- }
248
- while (i < prev.length) {
249
- ops[n++] = { kind: 'del' };
250
- i += 1;
251
- }
252
- while (j < next.length) {
253
- ops[n++] = { kind: 'ins', text: next[j] };
254
- j += 1;
255
206
  }
207
+ for (; i < prev.length; i++) ops.push({ kind: 'del' });
208
+ for (; j < next.length; j++) ops.push({ kind: 'ins', text: next[j] });
256
209
  return ops;
257
210
  };
258
211
 
@@ -261,13 +214,10 @@ const collapseRep = (ops) => {
261
214
  for (let i = 0; i < ops.length; i++) {
262
215
  const op = ops[i];
263
216
  const next = ops[i + 1];
264
- if (op.kind === 'del' && next && next.kind === 'ins') {
265
- out.push({ kind: 'rep', text: next.text });
266
- i += 1;
267
- continue;
268
- }
269
- if (op.kind === 'ins' && next && next.kind === 'del') {
270
- out.push({ kind: 'rep', text: op.text });
217
+ const delIns = op.kind === 'del' && next?.kind === 'ins';
218
+ const insDel = op.kind === 'ins' && next?.kind === 'del';
219
+ if (delIns || insDel) {
220
+ out.push({ kind: 'rep', text: delIns ? next.text : op.text });
271
221
  i += 1;
272
222
  continue;
273
223
  }
@@ -279,91 +229,44 @@ const collapseRep = (ops) => {
279
229
  const lineOps = (prev, next) => {
280
230
  if (prev.length === next.length) return zipOps(prev, next);
281
231
  if (prev.length * next.length > LCS_CELLS) return zipOps(prev, next);
282
- return collapseRep(walkLcs(prev, next, lcsTable(prev, next)));
232
+ return collapseRep(lcsOps(prev, next));
283
233
  };
284
234
 
285
- const copyRow = (row, text) => ({
286
- type: row.type,
287
- text,
288
- noNl: row.noNl === true,
289
- blockId: row.blockId ?? null,
290
- origin: row.origin ?? '',
291
- item: row.item ?? null,
292
- });
293
-
294
235
  const pushDels = (out, base, at) => {
295
236
  let i = at;
296
237
  while (i < base.length && base[i].type === 'del') {
297
- out.push(copyRow(base[i], base[i].text ?? ''));
238
+ out.push(tagged('del', base[i].text, base[i]));
298
239
  i += 1;
299
240
  }
300
241
  return i;
301
242
  };
302
243
 
303
- const mergeEditRows = (baseRows, nextTexts) => {
304
- const base = baseRows ?? [];
305
- const texts = nextTexts && nextTexts.length ? nextTexts : [''];
244
+ const mergeEditRows = (base, nextTexts) => {
245
+ const texts = nextTexts.length ? nextTexts : [''];
306
246
  const prev = [];
307
- for (const row of base) {
308
- if (row.type === 'del') continue;
309
- prev.push(row.text ?? '');
310
- }
311
- const ops = lineOps(prev, texts);
247
+ for (const row of base) if (row.type !== 'del') prev.push(row.text);
312
248
  const out = [];
313
249
  let at = 0;
314
- for (const op of ops) {
250
+ for (const op of lineOps(prev, texts)) {
315
251
  at = pushDels(out, base, at);
316
252
  if (op.kind === 'ins') {
317
- out.push(tagged('ctx', op.text, {}));
253
+ out.push(tagged('ctx', op.text, NO_EXTRA));
318
254
  continue;
319
255
  }
320
256
  if (at >= base.length) continue;
321
- if (op.kind === 'del') {
322
- at += 1;
323
- continue;
324
- }
325
- out.push(copyRow(base[at], op.text));
257
+ if (op.kind !== 'del') out.push(tagged(base[at].type, op.text, base[at]));
326
258
  at += 1;
327
259
  }
328
260
  pushDels(out, base, at);
329
- if (!out.length) out.push(tagged('ctx', '', {}));
261
+ if (!out.length) out.push(tagged('ctx', '', NO_EXTRA));
330
262
  return attachEditSpans(attachInline(out));
331
263
  };
332
264
 
333
- const fileLines = (text) => {
334
- const rows = splitFile(text);
335
- if (!rows.length) rows.push('');
336
- const { length } = rows;
337
- const out = new Array(length);
338
- let offset = 0;
339
- for (let i = 0; i < length; i++) {
340
- const row = rows[i];
341
- const start = offset;
342
- const end = start + row.length;
343
- const last = i === length - 1;
344
- out[i] = {
345
- type: 'ctx',
346
- text: row,
347
- noNl: false,
348
- wrap: true,
349
- editStart: start,
350
- editEnd: end,
351
- editLast: last,
352
- blockId: null,
353
- origin: '',
354
- item: null,
355
- };
356
- offset = last ? end : end + 1;
357
- }
358
- return out;
359
- };
360
-
361
265
  module.exports = {
362
266
  splitFile,
363
267
  splitEditor,
364
268
  itemsForPath,
365
269
  unitLines,
366
- sameBlock,
367
270
  sameHunk,
368
271
  hunkKey,
369
272
  blockLineRange,
@@ -371,7 +274,5 @@ module.exports = {
371
274
  blockLineIndex,
372
275
  fileOffset,
373
276
  fileLineIndex,
374
- attachEditSpans,
375
277
  mergeEditRows,
376
- fileLines,
377
278
  };
package/lib/editor.js CHANGED
@@ -6,6 +6,7 @@ const wrap = require('./wrap.js');
6
6
  const { wrapMove } = wrap;
7
7
 
8
8
  const UNDO_CAP = 100;
9
+ const WORD_RE = /[\p{L}\p{N}_]/u;
9
10
 
10
11
  const graphemeIndex = (text, cursor) => {
11
12
  const parts = graphemes(text);
@@ -24,10 +25,10 @@ const offsetAt = (parts, count) => {
24
25
  return pos;
25
26
  };
26
27
 
27
- const WORD_RE = /[\p{L}\p{N}_]/u;
28
-
29
28
  const isWordChar = (ch) => WORD_RE.test(ch);
30
29
 
30
+ const isSeparator = (ch) => !isWordChar(ch);
31
+
31
32
  const skipWhile = (parts, at, dir, pred) => {
32
33
  if (dir > 0) {
33
34
  while (at < parts.length && pred(parts[at])) at += 1;
@@ -38,20 +39,10 @@ const skipWhile = (parts, at, dir, pred) => {
38
39
  };
39
40
 
40
41
  const wordIndex = (parts, at, dir) => {
41
- const word = (ch) => isWordChar(ch);
42
- const sep = (ch) => !isWordChar(ch);
43
- if (dir > 0) {
44
- if (at < parts.length && word(parts[at])) {
45
- return skipWhile(parts, at, 1, word);
46
- }
47
- at = skipWhile(parts, at, 1, sep);
48
- return skipWhile(parts, at, 1, word);
49
- }
50
- if (at > 0 && word(parts[at - 1])) {
51
- return skipWhile(parts, at, -1, word);
52
- }
53
- at = skipWhile(parts, at, -1, sep);
54
- return skipWhile(parts, at, -1, word);
42
+ const ahead = dir > 0 ? parts[at] : parts[at - 1];
43
+ const inWord = ahead !== undefined && isWordChar(ahead);
44
+ const start = inWord ? at : skipWhile(parts, at, dir, isSeparator);
45
+ return skipWhile(parts, start, dir, isWordChar);
55
46
  };
56
47
 
57
48
  class Editor {
@@ -72,17 +63,22 @@ class Editor {
72
63
  restore(snap) {
73
64
  this.text = snap.text;
74
65
  this.cursor = snap.cursor;
75
- this.anchor = snap.anchor ?? null;
66
+ this.anchor = snap.anchor;
76
67
  }
77
68
 
78
69
  hasSelect() {
79
70
  return this.anchor !== null && this.anchor !== this.cursor;
80
71
  }
81
72
 
82
- selectedText() {
83
- if (!this.hasSelect()) return '';
73
+ span() {
74
+ if (!this.hasSelect()) return { lo: this.cursor, hi: this.cursor };
84
75
  const lo = Math.min(this.anchor, this.cursor);
85
76
  const hi = Math.max(this.anchor, this.cursor);
77
+ return { lo, hi };
78
+ }
79
+
80
+ selectedText() {
81
+ const { lo, hi } = this.span();
86
82
  return this.text.slice(lo, hi);
87
83
  }
88
84
 
@@ -95,26 +91,21 @@ class Editor {
95
91
  }
96
92
 
97
93
  collapse(delta) {
98
- const lo = Math.min(this.anchor, this.cursor);
99
- const hi = Math.max(this.anchor, this.cursor);
94
+ const { lo, hi } = this.span();
100
95
  this.anchor = null;
101
96
  this.cursor = delta < 0 ? lo : hi;
102
97
  }
103
98
 
104
99
  removeSpan() {
105
100
  if (!this.hasSelect()) return false;
106
- const lo = Math.min(this.anchor, this.cursor);
107
- const hi = Math.max(this.anchor, this.cursor);
108
- const next = this.text.slice(0, lo) + this.text.slice(hi);
109
- this.edit(next, lo);
101
+ const { lo, hi } = this.span();
102
+ this.edit(this.text.slice(0, lo) + this.text.slice(hi), lo);
110
103
  return true;
111
104
  }
112
105
 
113
106
  edit(nextText, nextCursor) {
114
107
  this.undo.push(this.snapshot());
115
- if (this.undo.length > UNDO_CAP) {
116
- this.undo = this.undo.slice(this.undo.length - UNDO_CAP);
117
- }
108
+ if (this.undo.length > UNDO_CAP) this.undo.shift();
118
109
  this.redo = [];
119
110
  this.text = nextText;
120
111
  this.cursor = nextCursor;
@@ -123,12 +114,9 @@ class Editor {
123
114
 
124
115
  insert(chunk) {
125
116
  this.wantCol = null;
126
- const selected = this.hasSelect();
127
- const lo = selected ? Math.min(this.anchor, this.cursor) : this.cursor;
128
- const hi = selected ? Math.max(this.anchor, this.cursor) : this.cursor;
129
- const before = this.text.slice(0, lo);
130
- const after = this.text.slice(hi);
131
- this.edit(before + chunk + after, lo + chunk.length);
117
+ const { lo, hi } = this.span();
118
+ const next = this.text.slice(0, lo) + chunk + this.text.slice(hi);
119
+ this.edit(next, lo + chunk.length);
132
120
  }
133
121
 
134
122
  replace(text) {
@@ -137,29 +125,29 @@ class Editor {
137
125
  }
138
126
 
139
127
  place(offset) {
140
- const next = Math.max(0, Math.min(offset, this.text.length));
141
128
  this.wantCol = null;
142
129
  this.anchor = null;
143
- this.cursor = next;
130
+ this.cursor = Math.max(0, Math.min(offset, this.text.length));
144
131
  }
145
132
 
146
133
  backspace() {
147
134
  this.wantCol = null;
148
135
  if (this.removeSpan()) return;
149
- const { parts, i } = graphemeIndex(this.text, this.cursor);
136
+ const { parts, i, pos } = graphemeIndex(this.text, this.cursor);
150
137
  if (i === 0) return;
151
- const drop = parts[i - 1];
152
- const nextParts = [...parts.slice(0, i - 1), ...parts.slice(i)];
153
- this.edit(nextParts.join(''), this.cursor - drop.length);
138
+ const size = parts[i - 1].length;
139
+ const next = this.text.slice(0, pos - size) + this.text.slice(pos);
140
+ this.edit(next, this.cursor - size);
154
141
  }
155
142
 
156
143
  delete() {
157
144
  this.wantCol = null;
158
145
  if (this.removeSpan()) return;
159
- const { parts, i } = graphemeIndex(this.text, this.cursor);
146
+ const { parts, i, pos } = graphemeIndex(this.text, this.cursor);
160
147
  if (i >= parts.length) return;
161
- const nextParts = [...parts.slice(0, i), ...parts.slice(i + 1)];
162
- this.edit(nextParts.join(''), this.cursor);
148
+ const next =
149
+ this.text.slice(0, pos) + this.text.slice(pos + parts[i].length);
150
+ this.edit(next, this.cursor);
163
151
  }
164
152
 
165
153
  move(delta, extend) {
@@ -183,15 +171,14 @@ class Editor {
183
171
 
184
172
  linePos() {
185
173
  const before = this.text.slice(0, this.cursor);
186
- const at = before.lastIndexOf('\n');
187
174
  const line = before.split('\n').length - 1;
188
- const col = at < 0 ? before.length : before.length - at - 1;
175
+ const col = before.length - before.lastIndexOf('\n') - 1;
189
176
  return { line, col };
190
177
  }
191
178
 
192
179
  visibleLineCol() {
193
180
  const { col, line } = this.linePos();
194
- const row = this.text.split('\n')[line] ?? '';
181
+ const row = this.text.split('\n')[line];
195
182
  return visibleWidth(row.slice(0, col));
196
183
  }
197
184
 
@@ -203,60 +190,56 @@ class Editor {
203
190
  }
204
191
 
205
192
  moveLine(delta, width, extend) {
206
- if (!extend) this.clearSelect();
207
- else this.beginSelect();
193
+ if (extend) this.beginSelect();
194
+ else this.clearSelect();
208
195
  if (width !== undefined) {
209
- const col = this.wantCol;
210
- const moved = wrapMove(this.text, this.cursor, width, delta, col);
196
+ const moved = wrapMove(
197
+ this.text,
198
+ this.cursor,
199
+ width,
200
+ delta,
201
+ this.wantCol,
202
+ );
211
203
  this.wantCol = moved.col;
212
204
  this.cursor = moved.cursor;
213
205
  return;
214
206
  }
215
207
  const rows = this.text.split('\n');
216
208
  const { line, col } = this.linePos();
217
- let next = line + delta;
218
- if (next < 0) next = 0;
219
- if (next >= rows.length) next = rows.length - 1;
209
+ const next = Math.min(Math.max(line + delta, 0), rows.length - 1);
220
210
  if (next === line) return;
221
- const row = rows[next];
222
- const c = Math.min(col, row.length);
223
211
  let cursor = 0;
224
212
  for (let i = 0; i < next; i++) cursor += rows[i].length + 1;
225
- this.cursor = cursor + c;
213
+ this.cursor = cursor + Math.min(col, rows[next].length);
226
214
  }
227
215
 
228
216
  home(extend) {
229
217
  this.wantCol = null;
230
- if (!extend) this.clearSelect();
231
- else this.beginSelect();
232
- const { col } = this.linePos();
233
- this.cursor -= col;
218
+ if (extend) this.beginSelect();
219
+ else this.clearSelect();
220
+ this.cursor -= this.linePos().col;
234
221
  }
235
222
 
236
223
  end(extend) {
237
224
  this.wantCol = null;
238
- if (!extend) this.clearSelect();
239
- else this.beginSelect();
225
+ if (extend) this.beginSelect();
226
+ else this.clearSelect();
240
227
  const { line, col } = this.linePos();
241
- const rows = this.text.split('\n');
242
- const row = rows[line] ?? '';
243
- this.cursor += row.length - col;
228
+ this.cursor += this.text.split('\n')[line].length - col;
244
229
  }
245
230
 
246
231
  undoEdit() {
247
232
  if (!this.undo.length) return;
248
233
  this.wantCol = null;
249
- const prev = this.undo.pop();
250
234
  this.redo.push(this.snapshot());
251
- this.restore(prev);
235
+ this.restore(this.undo.pop());
252
236
  }
253
237
 
254
238
  redoEdit() {
255
239
  if (!this.redo.length) return;
256
240
  this.wantCol = null;
257
- const next = this.redo.pop();
258
241
  this.undo.push(this.snapshot());
259
- this.restore(next);
242
+ this.restore(this.redo.pop());
260
243
  }
261
244
  }
262
245