pair-mode 0.1.0

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.
@@ -0,0 +1,2729 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ const require = createRequire(import.meta.url);
4
+
5
+ // src/tui/cli.ts
6
+ import { readFileSync } from "node:fs";
7
+
8
+ // src/adapters/entry-point.ts
9
+ import { realpathSync } from "node:fs";
10
+ import { fileURLToPath } from "node:url";
11
+ function isEntryPoint(moduleUrl) {
12
+ const entryArg = process.argv[1];
13
+ if (entryArg === void 0) {
14
+ return false;
15
+ }
16
+ try {
17
+ return realpathSync(entryArg) === realpathSync(fileURLToPath(moduleUrl));
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ // src/helpers/resultFilePath.ts
24
+ import { randomBytes } from "node:crypto";
25
+ import { tmpdir } from "node:os";
26
+ import { join } from "node:path";
27
+ var NAME_BYTES = 6;
28
+ function resultFilePath() {
29
+ const name = `pair-result-${randomBytes(NAME_BYTES).toString("hex")}.json`;
30
+ return join(tmpdir(), name);
31
+ }
32
+
33
+ // src/helpers/splitLines.ts
34
+ function splitLines(text) {
35
+ const lines = text.split("\n");
36
+ const last = lines.at(-1);
37
+ if (last === "") {
38
+ lines.pop();
39
+ }
40
+ return lines;
41
+ }
42
+
43
+ // src/core/marks.ts
44
+ var DEFAULT_CONTEXT = 5;
45
+ var DEFAULT_MIN_FOLD = 4;
46
+
47
+ // node_modules/.pnpm/diff@9.0.0/node_modules/diff/libesm/diff/base.js
48
+ var Diff = class {
49
+ diff(oldStr, newStr, options = {}) {
50
+ let callback;
51
+ if (typeof options === "function") {
52
+ callback = options;
53
+ options = {};
54
+ } else if ("callback" in options) {
55
+ callback = options.callback;
56
+ }
57
+ const oldString = this.castInput(oldStr, options);
58
+ const newString = this.castInput(newStr, options);
59
+ const oldTokens = this.removeEmpty(this.tokenize(oldString, options));
60
+ const newTokens = this.removeEmpty(this.tokenize(newString, options));
61
+ return this.diffWithOptionsObj(oldTokens, newTokens, options, callback);
62
+ }
63
+ diffWithOptionsObj(oldTokens, newTokens, options, callback) {
64
+ var _a;
65
+ const done = (value) => {
66
+ value = this.postProcess(value, options);
67
+ if (callback) {
68
+ setTimeout(function() {
69
+ callback(value);
70
+ }, 0);
71
+ return void 0;
72
+ } else {
73
+ return value;
74
+ }
75
+ };
76
+ const newLen = newTokens.length, oldLen = oldTokens.length;
77
+ let editLength = 1;
78
+ let maxEditLength = newLen + oldLen;
79
+ if (options.maxEditLength != null) {
80
+ maxEditLength = Math.min(maxEditLength, options.maxEditLength);
81
+ }
82
+ const maxExecutionTime = (_a = options.timeout) !== null && _a !== void 0 ? _a : Infinity;
83
+ const abortAfterTimestamp = Date.now() + maxExecutionTime;
84
+ const bestPath = [{ oldPos: -1, lastComponent: void 0 }];
85
+ let newPos = this.extractCommon(bestPath[0], newTokens, oldTokens, 0, options);
86
+ if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
87
+ return done(this.buildValues(bestPath[0].lastComponent, newTokens, oldTokens));
88
+ }
89
+ let minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
90
+ const execEditLength = () => {
91
+ for (let diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
92
+ let basePath;
93
+ const removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
94
+ if (removePath) {
95
+ bestPath[diagonalPath - 1] = void 0;
96
+ }
97
+ let canAdd = false;
98
+ if (addPath) {
99
+ const addPathNewPos = addPath.oldPos - diagonalPath;
100
+ canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
101
+ }
102
+ const canRemove = removePath && removePath.oldPos + 1 < oldLen;
103
+ if (!canAdd && !canRemove) {
104
+ bestPath[diagonalPath] = void 0;
105
+ continue;
106
+ }
107
+ if (!canRemove || canAdd && removePath.oldPos < addPath.oldPos) {
108
+ basePath = this.addToPath(addPath, true, false, 0, options);
109
+ } else {
110
+ basePath = this.addToPath(removePath, false, true, 1, options);
111
+ }
112
+ newPos = this.extractCommon(basePath, newTokens, oldTokens, diagonalPath, options);
113
+ if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
114
+ return done(this.buildValues(basePath.lastComponent, newTokens, oldTokens)) || true;
115
+ } else {
116
+ bestPath[diagonalPath] = basePath;
117
+ if (basePath.oldPos + 1 >= oldLen) {
118
+ maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
119
+ }
120
+ if (newPos + 1 >= newLen) {
121
+ minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
122
+ }
123
+ }
124
+ }
125
+ editLength++;
126
+ };
127
+ if (callback) {
128
+ (function exec() {
129
+ setTimeout(function() {
130
+ if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
131
+ return callback(void 0);
132
+ }
133
+ if (!execEditLength()) {
134
+ exec();
135
+ }
136
+ }, 0);
137
+ })();
138
+ } else {
139
+ while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
140
+ const ret = execEditLength();
141
+ if (ret) {
142
+ return ret;
143
+ }
144
+ }
145
+ }
146
+ }
147
+ addToPath(path, added, removed, oldPosInc, options) {
148
+ const last = path.lastComponent;
149
+ if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
150
+ return {
151
+ oldPos: path.oldPos + oldPosInc,
152
+ lastComponent: { count: last.count + 1, added, removed, previousComponent: last.previousComponent }
153
+ };
154
+ } else {
155
+ return {
156
+ oldPos: path.oldPos + oldPosInc,
157
+ lastComponent: { count: 1, added, removed, previousComponent: last }
158
+ };
159
+ }
160
+ }
161
+ extractCommon(basePath, newTokens, oldTokens, diagonalPath, options) {
162
+ const newLen = newTokens.length, oldLen = oldTokens.length;
163
+ let oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
164
+ while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(oldTokens[oldPos + 1], newTokens[newPos + 1], options)) {
165
+ newPos++;
166
+ oldPos++;
167
+ commonCount++;
168
+ if (options.oneChangePerToken) {
169
+ basePath.lastComponent = { count: 1, previousComponent: basePath.lastComponent, added: false, removed: false };
170
+ }
171
+ }
172
+ if (commonCount && !options.oneChangePerToken) {
173
+ basePath.lastComponent = { count: commonCount, previousComponent: basePath.lastComponent, added: false, removed: false };
174
+ }
175
+ basePath.oldPos = oldPos;
176
+ return newPos;
177
+ }
178
+ equals(left, right, options) {
179
+ if (options.comparator) {
180
+ return options.comparator(left, right);
181
+ } else {
182
+ return left === right || !!options.ignoreCase && left.toLowerCase() === right.toLowerCase();
183
+ }
184
+ }
185
+ removeEmpty(array) {
186
+ const ret = [];
187
+ for (let i = 0; i < array.length; i++) {
188
+ if (array[i]) {
189
+ ret.push(array[i]);
190
+ }
191
+ }
192
+ return ret;
193
+ }
194
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
195
+ castInput(value, options) {
196
+ return value;
197
+ }
198
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
199
+ tokenize(value, options) {
200
+ return Array.from(value);
201
+ }
202
+ join(chars) {
203
+ return chars.join("");
204
+ }
205
+ postProcess(changeObjects, options) {
206
+ return changeObjects;
207
+ }
208
+ get useLongestToken() {
209
+ return false;
210
+ }
211
+ buildValues(lastComponent, newTokens, oldTokens) {
212
+ const components = [];
213
+ let nextComponent;
214
+ while (lastComponent) {
215
+ components.push(lastComponent);
216
+ nextComponent = lastComponent.previousComponent;
217
+ delete lastComponent.previousComponent;
218
+ lastComponent = nextComponent;
219
+ }
220
+ components.reverse();
221
+ const componentLen = components.length;
222
+ let componentPos = 0, newPos = 0, oldPos = 0;
223
+ for (; componentPos < componentLen; componentPos++) {
224
+ const component = components[componentPos];
225
+ if (!component.removed) {
226
+ if (!component.added && this.useLongestToken) {
227
+ let value = newTokens.slice(newPos, newPos + component.count);
228
+ value = value.map(function(value2, i) {
229
+ const oldValue = oldTokens[oldPos + i];
230
+ return oldValue.length > value2.length ? oldValue : value2;
231
+ });
232
+ component.value = this.join(value);
233
+ } else {
234
+ component.value = this.join(newTokens.slice(newPos, newPos + component.count));
235
+ }
236
+ newPos += component.count;
237
+ if (!component.added) {
238
+ oldPos += component.count;
239
+ }
240
+ } else {
241
+ component.value = this.join(oldTokens.slice(oldPos, oldPos + component.count));
242
+ oldPos += component.count;
243
+ }
244
+ }
245
+ return components;
246
+ }
247
+ };
248
+
249
+ // node_modules/.pnpm/diff@9.0.0/node_modules/diff/libesm/util/string.js
250
+ function longestCommonPrefix(str1, str2) {
251
+ let i;
252
+ for (i = 0; i < str1.length && i < str2.length; i++) {
253
+ if (str1[i] != str2[i]) {
254
+ return str1.slice(0, i);
255
+ }
256
+ }
257
+ return str1.slice(0, i);
258
+ }
259
+ function longestCommonSuffix(str1, str2) {
260
+ let i;
261
+ if (!str1 || !str2 || str1[str1.length - 1] != str2[str2.length - 1]) {
262
+ return "";
263
+ }
264
+ for (i = 0; i < str1.length && i < str2.length; i++) {
265
+ if (str1[str1.length - (i + 1)] != str2[str2.length - (i + 1)]) {
266
+ return str1.slice(-i);
267
+ }
268
+ }
269
+ return str1.slice(-i);
270
+ }
271
+ function replacePrefix(string, oldPrefix, newPrefix) {
272
+ if (string.slice(0, oldPrefix.length) != oldPrefix) {
273
+ throw Error(`string ${JSON.stringify(string)} doesn't start with prefix ${JSON.stringify(oldPrefix)}; this is a bug`);
274
+ }
275
+ return newPrefix + string.slice(oldPrefix.length);
276
+ }
277
+ function replaceSuffix(string, oldSuffix, newSuffix) {
278
+ if (!oldSuffix) {
279
+ return string + newSuffix;
280
+ }
281
+ if (string.slice(-oldSuffix.length) != oldSuffix) {
282
+ throw Error(`string ${JSON.stringify(string)} doesn't end with suffix ${JSON.stringify(oldSuffix)}; this is a bug`);
283
+ }
284
+ return string.slice(0, -oldSuffix.length) + newSuffix;
285
+ }
286
+ function removePrefix(string, oldPrefix) {
287
+ return replacePrefix(string, oldPrefix, "");
288
+ }
289
+ function removeSuffix(string, oldSuffix) {
290
+ return replaceSuffix(string, oldSuffix, "");
291
+ }
292
+ function maximumOverlap(string1, string2) {
293
+ return string2.slice(0, overlapCount(string1, string2));
294
+ }
295
+ function overlapCount(a, b) {
296
+ let startA = 0;
297
+ if (a.length > b.length) {
298
+ startA = a.length - b.length;
299
+ }
300
+ let endB = b.length;
301
+ if (a.length < b.length) {
302
+ endB = a.length;
303
+ }
304
+ const map = Array(endB);
305
+ let k = 0;
306
+ map[0] = 0;
307
+ for (let j = 1; j < endB; j++) {
308
+ if (b[j] == b[k]) {
309
+ map[j] = map[k];
310
+ } else {
311
+ map[j] = k;
312
+ }
313
+ while (k > 0 && b[j] != b[k]) {
314
+ k = map[k];
315
+ }
316
+ if (b[j] == b[k]) {
317
+ k++;
318
+ }
319
+ }
320
+ k = 0;
321
+ for (let i = startA; i < a.length; i++) {
322
+ while (k > 0 && a[i] != b[k]) {
323
+ k = map[k];
324
+ }
325
+ if (a[i] == b[k]) {
326
+ k++;
327
+ }
328
+ }
329
+ return k;
330
+ }
331
+ function segment(string, segmenter) {
332
+ const parts = [];
333
+ for (const segmentObj of Array.from(segmenter.segment(string))) {
334
+ const segment2 = segmentObj.segment;
335
+ if (parts.length && /\s/.test(parts[parts.length - 1]) && /\s/.test(segment2)) {
336
+ parts[parts.length - 1] += segment2;
337
+ } else {
338
+ parts.push(segment2);
339
+ }
340
+ }
341
+ return parts;
342
+ }
343
+ function trailingWs(string, segmenter) {
344
+ if (segmenter) {
345
+ return leadingAndTrailingWs(string, segmenter)[1];
346
+ }
347
+ let i;
348
+ for (i = string.length - 1; i >= 0; i--) {
349
+ if (!string[i].match(/\s/)) {
350
+ break;
351
+ }
352
+ }
353
+ return string.substring(i + 1);
354
+ }
355
+ function leadingWs(string, segmenter) {
356
+ if (segmenter) {
357
+ return leadingAndTrailingWs(string, segmenter)[0];
358
+ }
359
+ const match = string.match(/^\s*/);
360
+ return match ? match[0] : "";
361
+ }
362
+ function leadingAndTrailingWs(string, segmenter) {
363
+ if (!segmenter) {
364
+ return [leadingWs(string), trailingWs(string)];
365
+ }
366
+ if (segmenter.resolvedOptions().granularity != "word") {
367
+ throw new Error('The segmenter passed must have a granularity of "word"');
368
+ }
369
+ const segments = segment(string, segmenter);
370
+ const firstSeg = segments[0];
371
+ const lastSeg = segments[segments.length - 1];
372
+ const head = /\s/.test(firstSeg) ? firstSeg : "";
373
+ const tail = /\s/.test(lastSeg) ? lastSeg : "";
374
+ return [head, tail];
375
+ }
376
+
377
+ // node_modules/.pnpm/diff@9.0.0/node_modules/diff/libesm/diff/word.js
378
+ var extendedWordChars = "a-zA-Z0-9_\\u{AD}\\u{C0}-\\u{D6}\\u{D8}-\\u{F6}\\u{F8}-\\u{2C6}\\u{2C8}-\\u{2D7}\\u{2DE}-\\u{2FF}\\u{1E00}-\\u{1EFF}";
379
+ var tokenizeIncludingWhitespace = new RegExp(`[${extendedWordChars}]+|\\s+|[^${extendedWordChars}]`, "ug");
380
+ var WordDiff = class extends Diff {
381
+ equals(left, right, options) {
382
+ if (options.ignoreCase) {
383
+ left = left.toLowerCase();
384
+ right = right.toLowerCase();
385
+ }
386
+ return left.trim() === right.trim();
387
+ }
388
+ tokenize(value, options = {}) {
389
+ let parts;
390
+ if (options.intlSegmenter) {
391
+ const segmenter = options.intlSegmenter;
392
+ if (segmenter.resolvedOptions().granularity != "word") {
393
+ throw new Error('The segmenter passed must have a granularity of "word"');
394
+ }
395
+ parts = segment(value, segmenter);
396
+ } else {
397
+ parts = value.match(tokenizeIncludingWhitespace) || [];
398
+ }
399
+ const tokens = [];
400
+ let prevPart = null;
401
+ parts.forEach((part) => {
402
+ if (/\s/.test(part)) {
403
+ if (prevPart == null) {
404
+ tokens.push(part);
405
+ } else {
406
+ tokens.push(tokens.pop() + part);
407
+ }
408
+ } else if (prevPart != null && /\s/.test(prevPart)) {
409
+ if (tokens[tokens.length - 1] == prevPart) {
410
+ tokens.push(tokens.pop() + part);
411
+ } else {
412
+ tokens.push(prevPart + part);
413
+ }
414
+ } else {
415
+ tokens.push(part);
416
+ }
417
+ prevPart = part;
418
+ });
419
+ return tokens;
420
+ }
421
+ join(tokens) {
422
+ return tokens.map((token, i) => {
423
+ if (i == 0) {
424
+ return token;
425
+ } else {
426
+ return token.replace(/^\s+/, "");
427
+ }
428
+ }).join("");
429
+ }
430
+ postProcess(changes, options) {
431
+ if (!changes || options.oneChangePerToken) {
432
+ return changes;
433
+ }
434
+ let lastKeep = null;
435
+ let insertion = null;
436
+ let deletion = null;
437
+ changes.forEach((change) => {
438
+ if (change.added) {
439
+ insertion = change;
440
+ } else if (change.removed) {
441
+ deletion = change;
442
+ } else {
443
+ if (insertion || deletion) {
444
+ dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, change, options.intlSegmenter);
445
+ }
446
+ lastKeep = change;
447
+ insertion = null;
448
+ deletion = null;
449
+ }
450
+ });
451
+ if (insertion || deletion) {
452
+ dedupeWhitespaceInChangeObjects(lastKeep, deletion, insertion, null, options.intlSegmenter);
453
+ }
454
+ return changes;
455
+ }
456
+ };
457
+ var wordDiff = new WordDiff();
458
+ function dedupeWhitespaceInChangeObjects(startKeep, deletion, insertion, endKeep, segmenter) {
459
+ if (deletion && insertion) {
460
+ const [oldWsPrefix, oldWsSuffix] = leadingAndTrailingWs(deletion.value, segmenter);
461
+ const [newWsPrefix, newWsSuffix] = leadingAndTrailingWs(insertion.value, segmenter);
462
+ if (startKeep) {
463
+ const commonWsPrefix = longestCommonPrefix(oldWsPrefix, newWsPrefix);
464
+ startKeep.value = replaceSuffix(startKeep.value, newWsPrefix, commonWsPrefix);
465
+ deletion.value = removePrefix(deletion.value, commonWsPrefix);
466
+ insertion.value = removePrefix(insertion.value, commonWsPrefix);
467
+ }
468
+ if (endKeep) {
469
+ const commonWsSuffix = longestCommonSuffix(oldWsSuffix, newWsSuffix);
470
+ endKeep.value = replacePrefix(endKeep.value, newWsSuffix, commonWsSuffix);
471
+ deletion.value = removeSuffix(deletion.value, commonWsSuffix);
472
+ insertion.value = removeSuffix(insertion.value, commonWsSuffix);
473
+ }
474
+ } else if (insertion) {
475
+ if (startKeep) {
476
+ const ws = leadingWs(insertion.value, segmenter);
477
+ insertion.value = insertion.value.substring(ws.length);
478
+ }
479
+ if (endKeep) {
480
+ const ws = leadingWs(endKeep.value, segmenter);
481
+ endKeep.value = endKeep.value.substring(ws.length);
482
+ }
483
+ } else if (startKeep && endKeep) {
484
+ const newWsFull = leadingWs(endKeep.value, segmenter), [delWsStart, delWsEnd] = leadingAndTrailingWs(deletion.value, segmenter);
485
+ const newWsStart = longestCommonPrefix(newWsFull, delWsStart);
486
+ deletion.value = removePrefix(deletion.value, newWsStart);
487
+ const newWsEnd = longestCommonSuffix(removePrefix(newWsFull, newWsStart), delWsEnd);
488
+ deletion.value = removeSuffix(deletion.value, newWsEnd);
489
+ endKeep.value = replacePrefix(endKeep.value, newWsFull, newWsEnd);
490
+ startKeep.value = replaceSuffix(startKeep.value, newWsFull, newWsFull.slice(0, newWsFull.length - newWsEnd.length));
491
+ } else if (endKeep) {
492
+ const endKeepWsPrefix = leadingWs(endKeep.value, segmenter);
493
+ const deletionWsSuffix = trailingWs(deletion.value, segmenter);
494
+ const overlap = maximumOverlap(deletionWsSuffix, endKeepWsPrefix);
495
+ deletion.value = removeSuffix(deletion.value, overlap);
496
+ } else if (startKeep) {
497
+ const startKeepWsSuffix = trailingWs(startKeep.value, segmenter);
498
+ const deletionWsPrefix = leadingWs(deletion.value, segmenter);
499
+ const overlap = maximumOverlap(startKeepWsSuffix, deletionWsPrefix);
500
+ deletion.value = removePrefix(deletion.value, overlap);
501
+ }
502
+ }
503
+ var WordsWithSpaceDiff = class extends Diff {
504
+ tokenize(value) {
505
+ const regex = new RegExp(`(\\r?\\n)|[${extendedWordChars}]+|[^\\S\\n\\r]+|[^${extendedWordChars}]`, "ug");
506
+ return value.match(regex) || [];
507
+ }
508
+ };
509
+ var wordsWithSpaceDiff = new WordsWithSpaceDiff();
510
+ function diffWordsWithSpace(oldStr, newStr, options) {
511
+ return wordsWithSpaceDiff.diff(oldStr, newStr, options);
512
+ }
513
+
514
+ // node_modules/.pnpm/diff@9.0.0/node_modules/diff/libesm/diff/array.js
515
+ var ArrayDiff = class extends Diff {
516
+ tokenize(value) {
517
+ return value.slice();
518
+ }
519
+ join(value) {
520
+ return value;
521
+ }
522
+ removeEmpty(value) {
523
+ return value;
524
+ }
525
+ };
526
+ var arrayDiff = new ArrayDiff();
527
+ function diffArrays(oldArr, newArr, options) {
528
+ return arrayDiff.diff(oldArr, newArr, options);
529
+ }
530
+
531
+ // src/tui/paint/layout.ts
532
+ import { basename } from "node:path";
533
+
534
+ // src/core/diff/diff.ts
535
+ function mergeChangedPair(first, second, i, j) {
536
+ const removed = first.removed ? first : second;
537
+ const added = first.added ? first : second;
538
+ const removedCount = removed.value.length;
539
+ const addedCount = added.value.length;
540
+ return { tag: "replace", i1: i, i2: i + removedCount, j1: j, j2: j + addedCount };
541
+ }
542
+ function opcodes(before, after) {
543
+ const chunks = diffArrays(before, after);
544
+ const result = [];
545
+ let i = 0;
546
+ let j = 0;
547
+ let index = 0;
548
+ while (index < chunks.length) {
549
+ const chunk = chunks[index];
550
+ if (chunk === void 0) {
551
+ break;
552
+ }
553
+ if (!chunk.added && !chunk.removed) {
554
+ const count = chunk.value.length;
555
+ result.push({ tag: "equal", i1: i, i2: i + count, j1: j, j2: j + count });
556
+ i += count;
557
+ j += count;
558
+ index += 1;
559
+ continue;
560
+ }
561
+ const next = chunks[index + 1];
562
+ const nextChanged = next !== void 0 && (next.added || next.removed);
563
+ const isPair = nextChanged && next !== void 0 && chunk.removed !== next.removed;
564
+ if (isPair && next !== void 0) {
565
+ const opcode = mergeChangedPair(chunk, next, i, j);
566
+ result.push(opcode);
567
+ i = opcode.i2;
568
+ j = opcode.j2;
569
+ index += 2;
570
+ continue;
571
+ }
572
+ if (chunk.removed) {
573
+ const removedCount = chunk.value.length;
574
+ result.push({ tag: "delete", i1: i, i2: i + removedCount, j1: j, j2: j });
575
+ i += removedCount;
576
+ index += 1;
577
+ continue;
578
+ }
579
+ const addedCount = chunk.value.length;
580
+ result.push({ tag: "insert", i1: i, i2: i, j1: j, j2: j + addedCount });
581
+ j += addedCount;
582
+ index += 1;
583
+ }
584
+ return result;
585
+ }
586
+
587
+ // src/tui/model/model.ts
588
+ var TAB_WIDTH = 8;
589
+ var CONTROL_BYTE_CEILING = 32;
590
+ var CONTROL_BYTE_PLACEHOLDER = "?";
591
+ function sanitizeLine(text) {
592
+ const scan = text.split("").reduce(
593
+ (state, char) => {
594
+ if (char === " ") {
595
+ const width = TAB_WIDTH - state.column % TAB_WIDTH;
596
+ return { output: state.output + " ".repeat(width), column: state.column + width };
597
+ }
598
+ if (char.charCodeAt(0) < CONTROL_BYTE_CEILING) {
599
+ return {
600
+ output: state.output + CONTROL_BYTE_PLACEHOLDER,
601
+ column: state.column + 1
602
+ };
603
+ }
604
+ return { output: state.output + char, column: state.column + 1 };
605
+ },
606
+ { output: "", column: 0 }
607
+ );
608
+ return scan.output;
609
+ }
610
+ function buildRows(before, after) {
611
+ return opcodes(before, after).flatMap((opcode) => {
612
+ const removed = before.slice(opcode.i1, opcode.i2);
613
+ const added = after.slice(opcode.j1, opcode.j2);
614
+ const rowCount = Math.max(removed.length, added.length);
615
+ return Array.from({ length: rowCount }, (_, row) => {
616
+ const removedLine = removed[row];
617
+ const addedLine = added[row];
618
+ const left = removedLine === void 0 ? "" : sanitizeLine(removedLine);
619
+ const right = addedLine === void 0 ? "" : sanitizeLine(addedLine);
620
+ const leftNumber = removedLine === void 0 ? null : opcode.i1 + row + 1;
621
+ const rightNumber = addedLine === void 0 ? null : opcode.j1 + row + 1;
622
+ const kind = opcode.tag === "equal" ? "context" : removedLine !== void 0 && addedLine !== void 0 ? "replace" : removedLine !== void 0 ? "del" : "add";
623
+ return { kind, left, right, leftNumber, rightNumber };
624
+ });
625
+ });
626
+ }
627
+ function buildFolds(rows, context, minFold) {
628
+ const keep = Array.from({ length: rows.length }).fill(false);
629
+ rows.forEach((row, index) => {
630
+ if (row.kind === "context") {
631
+ return;
632
+ }
633
+ const start = Math.max(0, index - context);
634
+ const end = Math.min(rows.length, index + context + 1);
635
+ keep.fill(true, start, end);
636
+ });
637
+ if (!keep.some((value) => value)) {
638
+ return [];
639
+ }
640
+ const runStarts = keep.flatMap(
641
+ (kept, index) => kept || index > 0 && keep[index - 1] === false ? [] : [index]
642
+ );
643
+ return runStarts.map((start) => {
644
+ const nextKept = keep.indexOf(true, start);
645
+ const end = nextKept === -1 ? rows.length : nextKept;
646
+ return { start, count: end - start, expanded: false };
647
+ }).filter((fold2) => fold2.count >= minFold);
648
+ }
649
+ function buildModel(before, after, context, minFold) {
650
+ const rows = buildRows(before, after);
651
+ const folds = buildFolds(rows, context, minFold);
652
+ return { rows, folds, cursor: 0 };
653
+ }
654
+ function computeVisibleRows(model) {
655
+ const foldByStart = new Map(
656
+ model.folds.map((foldGroup, foldIndex) => [foldGroup.start, foldIndex])
657
+ );
658
+ const hidden = new Set(
659
+ model.folds.flatMap(
660
+ (foldGroup) => foldGroup.expanded ? [] : Array.from({ length: foldGroup.count }, (_, offset) => foldGroup.start + offset)
661
+ )
662
+ );
663
+ return model.rows.flatMap((_, index) => {
664
+ const foldIndex = foldByStart.get(index);
665
+ const foldGroup = foldIndex === void 0 ? void 0 : model.folds[foldIndex];
666
+ if (foldGroup !== void 0 && foldIndex !== void 0 && !foldGroup.expanded) {
667
+ return [{ kind: "fold", foldIndex }];
668
+ }
669
+ return hidden.has(index) ? [] : [{ kind: "row", index }];
670
+ });
671
+ }
672
+ var visibleRowsCache = /* @__PURE__ */ new WeakMap();
673
+ function visibleRows(model) {
674
+ const byFolds = visibleRowsCache.get(model.rows) ?? /* @__PURE__ */ new WeakMap();
675
+ const cached = byFolds.get(model.folds);
676
+ if (cached !== void 0) {
677
+ return cached;
678
+ }
679
+ const computed = computeVisibleRows(model);
680
+ byFolds.set(model.folds, computed);
681
+ visibleRowsCache.set(model.rows, byFolds);
682
+ return computed;
683
+ }
684
+ function toggleFold(model, foldIndex) {
685
+ if (foldIndex < 0 || foldIndex >= model.folds.length) {
686
+ return model;
687
+ }
688
+ const folds = model.folds.map(
689
+ (foldGroup, index) => index === foldIndex ? { ...foldGroup, expanded: !foldGroup.expanded } : foldGroup
690
+ );
691
+ return { ...model, folds };
692
+ }
693
+ function moveCursor(model, delta) {
694
+ const visible = visibleRows(model);
695
+ if (visible.length === 0) {
696
+ return { ...model, cursor: 0 };
697
+ }
698
+ const clamped = Math.min(Math.max(model.cursor + delta, 0), visible.length - 1);
699
+ return { ...model, cursor: clamped };
700
+ }
701
+ function resolveClick(map, terminalRow, terminalColumn) {
702
+ const row = terminalRow - 1;
703
+ const column = terminalColumn - 1;
704
+ const screenRow = row >= 0 && row < map.rows.length ? map.rows[row] : void 0;
705
+ if (screenRow === void 0 || screenRow.kind === "chrome") {
706
+ return null;
707
+ }
708
+ if (screenRow.kind === "fold") {
709
+ return screenRow.index === null ? null : { kind: "fold", foldIndex: screenRow.index };
710
+ }
711
+ if (screenRow.index === null) {
712
+ return null;
713
+ }
714
+ const bounds = map.panes.find((pane) => column >= pane.textStart && column < pane.textEnd);
715
+ if (bounds === void 0) {
716
+ return null;
717
+ }
718
+ return {
719
+ kind: "row",
720
+ index: screenRow.index,
721
+ pane: bounds.pane,
722
+ column: column - bounds.textStart
723
+ };
724
+ }
725
+
726
+ // src/tui/selection/selection.ts
727
+ function visibleIndexForRow(model, rowIndex) {
728
+ const visible = visibleRows(model);
729
+ const found = visible.findIndex((entry) => entry.kind === "row" && entry.index === rowIndex);
730
+ return found === -1 ? model.cursor : found;
731
+ }
732
+ function currentRowIndex(model) {
733
+ const visible = visibleRows(model);
734
+ const entry = visible[model.cursor];
735
+ return entry !== void 0 && entry.kind === "row" ? entry.index : null;
736
+ }
737
+ function cursorRowIndex(model) {
738
+ return currentRowIndex(model);
739
+ }
740
+ function lastColumn(model, rowIndex, pane) {
741
+ return Math.max(paneLineLength(model, rowIndex, pane) - 1, 0);
742
+ }
743
+ function wholeRowSelection(model, rowIndex, pane) {
744
+ return {
745
+ pane,
746
+ anchorRow: rowIndex,
747
+ anchorColumn: 0,
748
+ headRow: rowIndex,
749
+ headColumn: lastColumn(model, rowIndex, pane)
750
+ };
751
+ }
752
+ function moveSelectionHead(state, delta) {
753
+ const selection = state.selection;
754
+ if (selection === null) {
755
+ return state;
756
+ }
757
+ const visible = visibleRows(state.model);
758
+ const currentIndex = visible.findIndex(
759
+ (entry2) => entry2.kind === "row" && entry2.index === selection.headRow
760
+ );
761
+ if (currentIndex === -1) {
762
+ return state;
763
+ }
764
+ const nextIndex = Math.min(Math.max(currentIndex + delta, 0), Math.max(visible.length - 1, 0));
765
+ const entry = visible[nextIndex];
766
+ const headRow = entry !== void 0 && entry.kind === "row" ? entry.index : selection.headRow;
767
+ const headColumn = lastColumn(state.model, headRow, selection.pane);
768
+ return { ...state, selection: { ...selection, headRow, headColumn } };
769
+ }
770
+ function startSelection(state) {
771
+ const rowIndex = currentRowIndex(state.model);
772
+ if (rowIndex === null) {
773
+ return state;
774
+ }
775
+ const selection = wholeRowSelection(state.model, rowIndex, "right");
776
+ return { ...state, selection, mode: "select" };
777
+ }
778
+ function normalizeSelection(selection) {
779
+ const reversed = selection.anchorRow > selection.headRow || selection.anchorRow === selection.headRow && selection.anchorColumn > selection.headColumn;
780
+ if (!reversed) {
781
+ return selection;
782
+ }
783
+ return {
784
+ pane: selection.pane,
785
+ anchorRow: selection.headRow,
786
+ anchorColumn: selection.headColumn,
787
+ headRow: selection.anchorRow,
788
+ headColumn: selection.anchorColumn
789
+ };
790
+ }
791
+ function clampSpan(span, lineLength) {
792
+ return {
793
+ start: Math.min(Math.max(span.start, 0), lineLength),
794
+ end: Math.min(Math.max(span.end, 0), lineLength)
795
+ };
796
+ }
797
+ function selectionSpanFor(selection, rowIndex, lineLength) {
798
+ if (selection === null) {
799
+ return null;
800
+ }
801
+ const normalized = normalizeSelection(selection);
802
+ if (rowIndex < normalized.anchorRow || rowIndex > normalized.headRow) {
803
+ return null;
804
+ }
805
+ if (normalized.anchorRow === normalized.headRow) {
806
+ return clampSpan(
807
+ { start: normalized.anchorColumn, end: normalized.headColumn + 1 },
808
+ lineLength
809
+ );
810
+ }
811
+ if (rowIndex === normalized.anchorRow) {
812
+ return clampSpan({ start: normalized.anchorColumn, end: lineLength }, lineLength);
813
+ }
814
+ if (rowIndex === normalized.headRow) {
815
+ return clampSpan({ start: 0, end: normalized.headColumn + 1 }, lineLength);
816
+ }
817
+ return clampSpan({ start: 0, end: lineLength }, lineLength);
818
+ }
819
+ function paneLineLength(model, rowIndex, pane) {
820
+ const row = model.rows[rowIndex];
821
+ if (row === void 0) {
822
+ return 0;
823
+ }
824
+ return pane === "left" ? row.left.length : row.right.length;
825
+ }
826
+ function applyMouseDown(state, target) {
827
+ if (target.kind === "fold") {
828
+ return { ...state, model: toggleFold(state.model, target.foldIndex) };
829
+ }
830
+ const selection = {
831
+ pane: target.pane,
832
+ anchorRow: target.index,
833
+ anchorColumn: target.column,
834
+ headRow: target.index,
835
+ headColumn: target.column
836
+ };
837
+ const model = { ...state.model, cursor: visibleIndexForRow(state.model, target.index) };
838
+ return { ...state, selection, mode: "select", model };
839
+ }
840
+ function applyMouseDrag(state, target) {
841
+ const selection = state.selection;
842
+ if (selection === null || target === null || target.kind !== "row") {
843
+ return state;
844
+ }
845
+ const samePane = target.pane === selection.pane;
846
+ const lineLength = paneLineLength(state.model, target.index, selection.pane);
847
+ const headColumn = samePane ? target.column : Math.min(Math.max(target.column, 0), lineLength);
848
+ const nextSelection = { ...selection, headRow: target.index, headColumn };
849
+ return { ...state, selection: nextSelection };
850
+ }
851
+ function applyMouseUp(state) {
852
+ const selection = state.selection;
853
+ if (selection === null) {
854
+ return { ...state, mode: "browse" };
855
+ }
856
+ const normalized = normalizeSelection(selection);
857
+ const isPlainClick = normalized.anchorRow === normalized.headRow && normalized.anchorColumn === normalized.headColumn;
858
+ return { ...state, selection: isPlainClick ? null : normalized, mode: "browse" };
859
+ }
860
+ function applyMouse(state, event) {
861
+ if (event.shift) {
862
+ return state;
863
+ }
864
+ if (event.kind === "scroll") {
865
+ return state;
866
+ }
867
+ if (event.kind === "down") {
868
+ const target = resolveClick(state.map, event.row, event.column);
869
+ if (target === null) {
870
+ return state;
871
+ }
872
+ return applyMouseDown(state, target);
873
+ }
874
+ if (event.kind === "drag") {
875
+ const target = resolveClick(state.map, event.row, event.column);
876
+ return applyMouseDrag(state, target);
877
+ }
878
+ return applyMouseUp(state);
879
+ }
880
+
881
+ // src/helpers/hexColor.ts
882
+ var HEX_COLOR = /^#[0-9a-fA-F]{6}$/;
883
+ function isHexColor(value) {
884
+ return HEX_COLOR.test(value);
885
+ }
886
+
887
+ // src/tui/paint/theme.ts
888
+ var RED_CHANNEL_START = 1;
889
+ var GREEN_CHANNEL_START = 3;
890
+ var BLUE_CHANNEL_START = 5;
891
+ var CHANNEL_END = 7;
892
+ var HEX_RADIX = 16;
893
+ var theme = Object.freeze({
894
+ addBar: "#3FB950",
895
+ addSpan: "#1F5B2E",
896
+ delBar: "#F85149",
897
+ delSpan: "#6B2126",
898
+ selection: "#16324F",
899
+ note: "#D2A8FF",
900
+ fold: "#6E7681",
901
+ chrome: "#E8A33D",
902
+ statusText: "#1F1A12"
903
+ });
904
+ var RESET = "\x1B[0m";
905
+ var DEFAULT_FG = "\x1B[39m";
906
+ var DEFAULT_BG = "\x1B[49m";
907
+ var ANSI_16 = {
908
+ [theme.addBar]: { fg: "\x1B[32m", bg: "\x1B[42m" },
909
+ [theme.addSpan]: { fg: "\x1B[32m", bg: "\x1B[42m" },
910
+ [theme.delBar]: { fg: "\x1B[31m", bg: "\x1B[41m" },
911
+ [theme.delSpan]: { fg: "\x1B[31m", bg: "\x1B[41m" },
912
+ [theme.selection]: { fg: "\x1B[34m", bg: "\x1B[44m" },
913
+ [theme.note]: { fg: "\x1B[35m", bg: "\x1B[45m" },
914
+ [theme.fold]: { fg: "\x1B[90m", bg: "\x1B[100m" },
915
+ [theme.chrome]: { fg: "\x1B[33m", bg: "\x1B[43m" },
916
+ [theme.statusText]: { fg: "\x1B[30m", bg: "\x1B[40m" }
917
+ };
918
+ function supportsTruecolor(env) {
919
+ return env.COLORTERM === "truecolor" || env.COLORTERM === "24bit";
920
+ }
921
+ function hexToRgb(hex) {
922
+ if (!isHexColor(hex)) {
923
+ return null;
924
+ }
925
+ return {
926
+ r: parseInt(hex.slice(RED_CHANNEL_START, GREEN_CHANNEL_START), HEX_RADIX),
927
+ g: parseInt(hex.slice(GREEN_CHANNEL_START, BLUE_CHANNEL_START), HEX_RADIX),
928
+ b: parseInt(hex.slice(BLUE_CHANNEL_START, CHANNEL_END), HEX_RADIX)
929
+ };
930
+ }
931
+ function fg(hex, truecolor) {
932
+ const rgb = hexToRgb(hex);
933
+ if (rgb === null) {
934
+ return DEFAULT_FG;
935
+ }
936
+ if (truecolor) {
937
+ return `\x1B[38;2;${rgb.r};${rgb.g};${rgb.b}m`;
938
+ }
939
+ const mapped = ANSI_16[hex];
940
+ return mapped === void 0 ? DEFAULT_FG : mapped.fg;
941
+ }
942
+ function bg(hex, truecolor) {
943
+ const rgb = hexToRgb(hex);
944
+ if (rgb === null) {
945
+ return DEFAULT_BG;
946
+ }
947
+ if (truecolor) {
948
+ return `\x1B[48;2;${rgb.r};${rgb.g};${rgb.b}m`;
949
+ }
950
+ const mapped = ANSI_16[hex];
951
+ return mapped === void 0 ? DEFAULT_BG : mapped.bg;
952
+ }
953
+
954
+ // src/tui/paint/layout.ts
955
+ var NUMBER_WIDTH_FLOOR = 2;
956
+ var GUTTER_SPACE_WIDTH = 1;
957
+ var SIGN_BAR_WIDTH = 1;
958
+ var DIVIDER_WIDTH = 1;
959
+ var PANE_COUNT = 2;
960
+ var HEADER_ROWS = 2;
961
+ var STATUS_ROWS = 1;
962
+ var HEADER_COUNT_GAP_WIDTH = 1;
963
+ var UNIFIED_PANE_COUNT = 1;
964
+ var TEXT_GAP_WIDTH = 1;
965
+ var MIN_BODY_HEIGHT = 1;
966
+ var MARKER_WIDTH = 1;
967
+ var NOTE_MARKER = "\u25CF";
968
+ var FOCUSED_NOTE_MARKER = "\u25B8";
969
+ var PANEL_TITLE_ROWS = 1;
970
+ var PANEL_DRAFT_ROWS = 1;
971
+ var PANEL_MAX_HEIGHT = 6;
972
+ var CONFIRM_PANEL_HEIGHT = 2;
973
+ var CURSOR_WIDTH = 1;
974
+ var NO_PANEL_HEIGHT = 0;
975
+ var ANCHORED_CONNECTOR = "\u2570\u2500";
976
+ var ANCHORED_FOCUSED_CONNECTOR = "\u2570\u25B8";
977
+ var ANCHORED_CONNECTOR_GAP = " ";
978
+ var UNIFIED_REPLACE_LINES = 2;
979
+ var SINGLE_LINE = 1;
980
+ var MIN_PAGE_ROWS = 1;
981
+ var NO_SPANS = { left: [], right: [] };
982
+ var SIGN_BAR = {
983
+ context: { leftChar: " ", leftColor: null, rightChar: " ", rightColor: null },
984
+ add: { leftChar: " ", leftColor: null, rightChar: "\u258C", rightColor: theme.addBar },
985
+ del: { leftChar: "\u258C", leftColor: theme.delBar, rightChar: " ", rightColor: null },
986
+ replace: { leftChar: "\u258C", leftColor: theme.delBar, rightChar: "\u258C", rightColor: theme.addBar }
987
+ };
988
+ function panelHeight(noteCount, mode, notePosition) {
989
+ if (mode === "confirm") {
990
+ return CONFIRM_PANEL_HEIGHT;
991
+ }
992
+ if (notePosition === "anchored") {
993
+ return NO_PANEL_HEIGHT;
994
+ }
995
+ const draftRows = mode === "note" ? PANEL_DRAFT_ROWS : 0;
996
+ const rowCount = noteCount + draftRows;
997
+ return rowCount === 0 ? 0 : Math.min(rowCount + PANEL_TITLE_ROWS, PANEL_MAX_HEIGHT);
998
+ }
999
+ function bodyHeight(height, noteCount, mode, notePosition) {
1000
+ return Math.max(
1001
+ height - HEADER_ROWS - STATUS_ROWS - panelHeight(noteCount, mode, notePosition),
1002
+ MIN_BODY_HEIGHT
1003
+ );
1004
+ }
1005
+ function isUnified(geometry) {
1006
+ return geometry.layout === "unified" || geometry.width < MIN_SPLIT_WIDTH || !hasRemovals(geometry.model);
1007
+ }
1008
+ function anchoredExtraLines(geometry, rowIndex) {
1009
+ if (geometry.notePosition !== "anchored") {
1010
+ return 0;
1011
+ }
1012
+ const noteLines = geometry.notes.filter((note) => note.endRowIndex === rowIndex).length;
1013
+ const draftLines = geometry.mode === "note" && draftAnchorRowFor(geometry.selection) === rowIndex ? 1 : 0;
1014
+ return noteLines + draftLines;
1015
+ }
1016
+ function entryLineCount(geometry, entry) {
1017
+ if (entry.kind === "fold") {
1018
+ return SINGLE_LINE;
1019
+ }
1020
+ const row = lookupRow(geometry.model, entry.index);
1021
+ const base = isUnified(geometry) && row.kind === "replace" ? UNIFIED_REPLACE_LINES : SINGLE_LINE;
1022
+ return base + anchoredExtraLines(geometry, entry.index);
1023
+ }
1024
+ function geometryBodyRows(geometry) {
1025
+ return bodyHeight(geometry.height, geometry.notes.length, geometry.mode, geometry.notePosition);
1026
+ }
1027
+ function rowsThatFit(geometry, scrollTop) {
1028
+ const visible = visibleRows(geometry.model);
1029
+ const bodyRows = geometryBodyRows(geometry);
1030
+ let used = 0;
1031
+ let count = 0;
1032
+ for (let index = scrollTop; index < visible.length; index += 1) {
1033
+ const cost = entryLineCount(geometry, visible[index]);
1034
+ if (count > 0 && used + cost > bodyRows) {
1035
+ break;
1036
+ }
1037
+ used += cost;
1038
+ count += 1;
1039
+ if (used >= bodyRows) {
1040
+ break;
1041
+ }
1042
+ }
1043
+ return count;
1044
+ }
1045
+ function lastFittingRow(geometry, scrollTop) {
1046
+ return scrollTop + Math.max(rowsThatFit(geometry, scrollTop), SINGLE_LINE) - 1;
1047
+ }
1048
+ function scrollTopForRow(geometry, row) {
1049
+ const visible = visibleRows(geometry.model);
1050
+ const entry = visible[row];
1051
+ if (entry === void 0) {
1052
+ return 0;
1053
+ }
1054
+ const bodyRows = geometryBodyRows(geometry);
1055
+ let used = entryLineCount(geometry, entry);
1056
+ let top = row;
1057
+ for (let index = row - 1; index >= 0; index -= 1) {
1058
+ const cost = entryLineCount(geometry, visible[index]);
1059
+ if (used + cost > bodyRows) {
1060
+ break;
1061
+ }
1062
+ used += cost;
1063
+ top = index;
1064
+ }
1065
+ return top;
1066
+ }
1067
+ function followScrollTop(geometry, scrollTop) {
1068
+ const cursor = geometry.model.cursor;
1069
+ if (cursor < scrollTop) {
1070
+ return cursor;
1071
+ }
1072
+ return cursor <= lastFittingRow(geometry, scrollTop) ? scrollTop : scrollTopForRow(geometry, cursor);
1073
+ }
1074
+ function maxScrollTop(geometry) {
1075
+ const visible = visibleRows(geometry.model);
1076
+ return visible.length === 0 ? 0 : scrollTopForRow(geometry, visible.length - 1);
1077
+ }
1078
+ function pageRowStep(geometry, scrollTop) {
1079
+ return Math.max(lastFittingRow(geometry, scrollTop) - scrollTop, MIN_PAGE_ROWS);
1080
+ }
1081
+ function hasPaneNotes(notes, pane) {
1082
+ return notes.some((note) => note.pane === pane);
1083
+ }
1084
+ function coversRow(note, rowIndex, pane) {
1085
+ return note.pane === pane && rowIndex >= note.rowIndex && rowIndex <= note.endRowIndex;
1086
+ }
1087
+ function noteSpanFor(note, rowIndex, lineLength) {
1088
+ const start = rowIndex === note.rowIndex ? note.startColumn : 0;
1089
+ const end = rowIndex === note.endRowIndex ? note.endColumn : lineLength;
1090
+ return {
1091
+ start: Math.min(Math.max(start, 0), lineLength),
1092
+ end: Math.min(Math.max(end, 0), lineLength)
1093
+ };
1094
+ }
1095
+ function noteSpansFor(notes, rowIndex, pane, lineLength) {
1096
+ return notes.filter((note) => coversRow(note, rowIndex, pane)).map((note) => noteSpanFor(note, rowIndex, lineLength));
1097
+ }
1098
+ function isAnnotatedRow(notes, rowIndex, pane) {
1099
+ return notes.some((note) => coversRow(note, rowIndex, pane));
1100
+ }
1101
+ function paintMarkerColumn(hasColumn, annotated, truecolor) {
1102
+ if (!hasColumn) {
1103
+ return "";
1104
+ }
1105
+ return annotated ? fg(theme.note, truecolor) + NOTE_MARKER + DEFAULT_FG : " ";
1106
+ }
1107
+ function widestNumber(rows) {
1108
+ return rows.reduce(
1109
+ (widest, row) => Math.max(widest, row.leftNumber ?? 0, row.rightNumber ?? 0),
1110
+ 0
1111
+ );
1112
+ }
1113
+ var numberWidthCache = /* @__PURE__ */ new WeakMap();
1114
+ function computeNumberWidth(model) {
1115
+ const cached = numberWidthCache.get(model.rows);
1116
+ if (cached !== void 0) {
1117
+ return cached;
1118
+ }
1119
+ const width = Math.max(NUMBER_WIDTH_FLOOR, String(widestNumber(model.rows)).length);
1120
+ numberWidthCache.set(model.rows, width);
1121
+ return width;
1122
+ }
1123
+ var changeCountCache = /* @__PURE__ */ new WeakMap();
1124
+ function changeCounts(model) {
1125
+ const cached = changeCountCache.get(model.rows);
1126
+ if (cached !== void 0) {
1127
+ return cached;
1128
+ }
1129
+ const counts = model.rows.reduce(
1130
+ (running, row) => ({
1131
+ add: running.add + (row.kind === "add" || row.kind === "replace" ? 1 : 0),
1132
+ del: running.del + (row.kind === "del" || row.kind === "replace" ? 1 : 0)
1133
+ }),
1134
+ { add: 0, del: 0 }
1135
+ );
1136
+ changeCountCache.set(model.rows, counts);
1137
+ return counts;
1138
+ }
1139
+ function lookupRow(model, index) {
1140
+ const row = model.rows[index];
1141
+ if (row === void 0) {
1142
+ throw new Error(`paintSplit: row index out of bounds: ${index}`);
1143
+ }
1144
+ return row;
1145
+ }
1146
+ function lookupFold(model, foldIndex) {
1147
+ const fold2 = model.folds[foldIndex];
1148
+ if (fold2 === void 0) {
1149
+ throw new Error(`paintSplit: fold index out of bounds: ${foldIndex}`);
1150
+ }
1151
+ return fold2;
1152
+ }
1153
+ function paintGutter(lineNumber, numberWidth, truecolor, cursorRow) {
1154
+ const text = lineNumber === null ? "" : String(lineNumber);
1155
+ const color = cursorRow ? theme.chrome : theme.fold;
1156
+ return fg(color, truecolor) + text.padStart(numberWidth, " ") + " " + DEFAULT_FG;
1157
+ }
1158
+ function paintSignBar(char, color, truecolor) {
1159
+ return color === null ? DEFAULT_FG + char : fg(color, truecolor) + char;
1160
+ }
1161
+ function mergeSpans(spans) {
1162
+ if (spans.length < 2) {
1163
+ return spans;
1164
+ }
1165
+ return [...spans].sort((left, right) => left.start - right.start).reduce((merged, span) => {
1166
+ const last = merged[merged.length - 1];
1167
+ if (last === void 0 || span.start > last.end) {
1168
+ return [...merged, span];
1169
+ }
1170
+ return [...merged.slice(0, -1), { start: last.start, end: Math.max(last.end, span.end) }];
1171
+ }, []);
1172
+ }
1173
+ function renderPaneText(text, tokens, changeSpansForSide, changeColor, paneWidth, rowBand, truecolor, highlightSpans) {
1174
+ const textLength = Math.min(text.length, paneWidth);
1175
+ const highlights = mergeSpans(highlightSpans);
1176
+ const parts = [];
1177
+ let currentFg = void 0;
1178
+ let currentBg = void 0;
1179
+ let tokenCursor = 0;
1180
+ let spanCursor = 0;
1181
+ let highlightCursor = 0;
1182
+ for (let column = 0; column < paneWidth; column += 1) {
1183
+ while (tokenCursor < tokens.length && tokens[tokenCursor].end <= column) {
1184
+ tokenCursor += 1;
1185
+ }
1186
+ while (spanCursor < changeSpansForSide.length && changeSpansForSide[spanCursor].end <= column) {
1187
+ spanCursor += 1;
1188
+ }
1189
+ while (highlightCursor < highlights.length && highlights[highlightCursor].end <= column) {
1190
+ highlightCursor += 1;
1191
+ }
1192
+ const token = tokens[tokenCursor];
1193
+ const span = changeSpansForSide[spanCursor];
1194
+ const highlight = highlights[highlightCursor];
1195
+ const char = column < textLength ? text.charAt(column) : " ";
1196
+ const desiredFg = column < textLength && token !== void 0 && column >= token.start ? token.color : null;
1197
+ const withinSpan = span !== void 0 && column >= span.start;
1198
+ const withinHighlight = highlight !== void 0 && column >= highlight.start;
1199
+ const desiredBg = withinHighlight ? theme.selection : changeColor !== null && (rowBand || withinSpan) ? changeColor : null;
1200
+ if (desiredFg !== currentFg) {
1201
+ parts.push(desiredFg === null ? DEFAULT_FG : fg(desiredFg, truecolor));
1202
+ currentFg = desiredFg;
1203
+ }
1204
+ if (desiredBg !== currentBg) {
1205
+ parts.push(desiredBg === null ? DEFAULT_BG : bg(desiredBg, truecolor));
1206
+ currentBg = desiredBg;
1207
+ }
1208
+ parts.push(char);
1209
+ }
1210
+ return parts.join("");
1211
+ }
1212
+ function paintModelRow(row, rowIndex, leftBounds, rightBounds, numberWidth, tokens, rowBand, changeBackground, truecolor, selection, notes, hasLeftMarkerColumn, hasRightMarkerColumn, cursorRow) {
1213
+ const bar = SIGN_BAR[row.kind];
1214
+ const spans = row.kind === "context" ? NO_SPANS : changedSpans(row.left, row.right);
1215
+ const leftColor = changeBackground && (row.kind === "del" || row.kind === "replace") ? theme.delSpan : null;
1216
+ const rightColor = changeBackground && (row.kind === "add" || row.kind === "replace") ? theme.addSpan : null;
1217
+ const leftPaneWidth = leftBounds.textEnd - leftBounds.textStart;
1218
+ const rightPaneWidth = rightBounds.textEnd - rightBounds.textStart;
1219
+ const leftSelectionSpan = selection !== null && selection.pane === "left" ? selectionSpanFor(selection, rowIndex, row.left.length) : null;
1220
+ const rightSelectionSpan = selection !== null && selection.pane === "right" ? selectionSpanFor(selection, rowIndex, row.right.length) : null;
1221
+ const leftHighlights = [
1222
+ ...leftSelectionSpan === null ? [] : [leftSelectionSpan],
1223
+ ...noteSpansFor(notes, rowIndex, "left", row.left.length)
1224
+ ];
1225
+ const rightHighlights = [
1226
+ ...rightSelectionSpan === null ? [] : [rightSelectionSpan],
1227
+ ...noteSpansFor(notes, rowIndex, "right", row.right.length)
1228
+ ];
1229
+ const leftText = renderPaneText(
1230
+ row.left,
1231
+ tokens(row.left, row.leftNumber),
1232
+ spans.left,
1233
+ leftColor,
1234
+ leftPaneWidth,
1235
+ rowBand,
1236
+ truecolor,
1237
+ leftHighlights
1238
+ );
1239
+ const rightText = renderPaneText(
1240
+ row.right,
1241
+ tokens(row.right, row.rightNumber),
1242
+ spans.right,
1243
+ rightColor,
1244
+ rightPaneWidth,
1245
+ rowBand,
1246
+ truecolor,
1247
+ rightHighlights
1248
+ );
1249
+ const divider = fg(theme.fold, truecolor) + "\u2502" + DEFAULT_FG;
1250
+ return paintGutter(row.leftNumber, numberWidth, truecolor, cursorRow) + paintSignBar(bar.leftChar, bar.leftColor, truecolor) + paintMarkerColumn(hasLeftMarkerColumn, isAnnotatedRow(notes, rowIndex, "left"), truecolor) + leftText + divider + paintGutter(row.rightNumber, numberWidth, truecolor, cursorRow) + paintSignBar(bar.rightChar, bar.rightColor, truecolor) + paintMarkerColumn(hasRightMarkerColumn, isAnnotatedRow(notes, rowIndex, "right"), truecolor) + rightText + RESET;
1251
+ }
1252
+ function paintFoldRow(fold2, width, truecolor, cursorRow) {
1253
+ const label = `\u22EF ${fold2.count} unchanged lines`;
1254
+ const totalPadding = Math.max(0, width - label.length);
1255
+ const leftPadding = Math.floor(totalPadding / 2);
1256
+ const rightPadding = totalPadding - leftPadding;
1257
+ const color = cursorRow ? theme.chrome : theme.fold;
1258
+ return fg(color, truecolor) + " ".repeat(leftPadding) + label + " ".repeat(rightPadding) + RESET;
1259
+ }
1260
+ function paintHeader(path, addCount, delCount, width, truecolor) {
1261
+ const prefix = `pair mode\u2502${basename(path)}\u2502`;
1262
+ const addText = `+${addCount}`;
1263
+ const delText = `-${delCount}`;
1264
+ const used = prefix.length + addText.length + HEADER_COUNT_GAP_WIDTH + delText.length;
1265
+ const padding = " ".repeat(Math.max(0, width - used));
1266
+ return fg(theme.chrome, truecolor) + prefix + fg(theme.addBar, truecolor) + addText + fg(theme.chrome, truecolor) + " ".repeat(HEADER_COUNT_GAP_WIDTH) + fg(theme.delBar, truecolor) + delText + fg(theme.chrome, truecolor) + padding + RESET;
1267
+ }
1268
+ function paintRule(width, truecolor) {
1269
+ return fg(theme.fold, truecolor) + "\u2500".repeat(width) + RESET;
1270
+ }
1271
+ var KEY_HINTS = [
1272
+ "j/k move",
1273
+ "^d/^u page",
1274
+ "n/N hunk",
1275
+ "v select",
1276
+ "a note",
1277
+ "tab cycle",
1278
+ "d delete",
1279
+ "space fold",
1280
+ "u layout",
1281
+ "s send",
1282
+ "q quit",
1283
+ "? keys"
1284
+ ];
1285
+ var HINT_SEPARATOR = " \xB7 ";
1286
+ function fitHints(width) {
1287
+ return KEY_HINTS.reduce((kept, hint) => {
1288
+ const candidate = kept === "" ? hint : kept + HINT_SEPARATOR + hint;
1289
+ return candidate.length <= width ? candidate : kept;
1290
+ }, "");
1291
+ }
1292
+ function paintStatus(width, truecolor, message) {
1293
+ const text = message === null ? fitHints(width) : message.slice(0, width);
1294
+ const padding = " ".repeat(Math.max(0, width - text.length));
1295
+ return bg(theme.chrome, truecolor) + fg(theme.statusText, truecolor) + text + padding + RESET;
1296
+ }
1297
+ function paintBlankLine(width) {
1298
+ return " ".repeat(width) + RESET;
1299
+ }
1300
+ function padPanelLine(content, contentLength, width) {
1301
+ return content + " ".repeat(Math.max(0, width - contentLength)) + RESET;
1302
+ }
1303
+ function paintPanelTitle(noteCount, width, truecolor) {
1304
+ const text = `NOTES (${noteCount})`.slice(0, width);
1305
+ return padPanelLine(fg(theme.note, truecolor) + text + DEFAULT_FG, text.length, width);
1306
+ }
1307
+ function lineLabel(line) {
1308
+ return line === null ? "?" : String(line);
1309
+ }
1310
+ function noteAnchorLabel(note) {
1311
+ return note.endRowIndex === note.rowIndex ? `L${lineLabel(note.line)}` : `L${lineLabel(note.line)}-${lineLabel(note.endLine)}`;
1312
+ }
1313
+ function paintNoteRow(note, focused, width, truecolor) {
1314
+ const marker = focused ? FOCUSED_NOTE_MARKER : NOTE_MARKER;
1315
+ const rest = ` ${noteAnchorLabel(note)} ${note.text}`.slice(0, Math.max(0, width - MARKER_WIDTH));
1316
+ return padPanelLine(
1317
+ fg(theme.note, truecolor) + marker + DEFAULT_FG + rest,
1318
+ marker.length + rest.length,
1319
+ width
1320
+ );
1321
+ }
1322
+ function paintDraftRow(draft, width, truecolor) {
1323
+ const text = draft.slice(0, Math.max(0, width - CURSOR_WIDTH));
1324
+ const cursor = bg(theme.chrome, truecolor) + " " + DEFAULT_BG;
1325
+ return padPanelLine(text + cursor, text.length + CURSOR_WIDTH, width);
1326
+ }
1327
+ function paintAnchoredNoteRow(note, focused, width, truecolor) {
1328
+ const connector = focused ? ANCHORED_FOCUSED_CONNECTOR : ANCHORED_CONNECTOR;
1329
+ const maxTextWidth = Math.max(0, width - connector.length - ANCHORED_CONNECTOR_GAP.length);
1330
+ const text = note.text.slice(0, maxTextWidth);
1331
+ const content = fg(theme.fold, truecolor) + connector + DEFAULT_FG + ANCHORED_CONNECTOR_GAP + fg(theme.note, truecolor) + text + DEFAULT_FG;
1332
+ const plainLength = connector.length + ANCHORED_CONNECTOR_GAP.length + text.length;
1333
+ return padPanelLine(content, plainLength, width);
1334
+ }
1335
+ function paintAnchoredDraftRow(draft, width, truecolor) {
1336
+ const connector = ANCHORED_CONNECTOR;
1337
+ const maxTextWidth = Math.max(
1338
+ 0,
1339
+ width - connector.length - ANCHORED_CONNECTOR_GAP.length - CURSOR_WIDTH
1340
+ );
1341
+ const text = draft.slice(0, maxTextWidth);
1342
+ const cursor = bg(theme.chrome, truecolor) + " " + DEFAULT_BG;
1343
+ const content = fg(theme.fold, truecolor) + connector + DEFAULT_FG + ANCHORED_CONNECTOR_GAP + text + cursor;
1344
+ const plainLength = connector.length + ANCHORED_CONNECTOR_GAP.length + text.length + CURSOR_WIDTH;
1345
+ return padPanelLine(content, plainLength, width);
1346
+ }
1347
+ function draftAnchorRowFor(selection) {
1348
+ return selection === null ? null : Math.min(selection.anchorRow, selection.headRow);
1349
+ }
1350
+ function anchoredNoteRowsFor(rowIndex, notes, focusedNote, width, truecolor) {
1351
+ const rowNotes = notes.filter((note) => note.endRowIndex === rowIndex).sort((left, right) => left.id - right.id);
1352
+ const chromeRow = { kind: "chrome", index: null };
1353
+ return {
1354
+ lines: rowNotes.map(
1355
+ (note) => paintAnchoredNoteRow(note, note.id === focusedNote, width, truecolor)
1356
+ ),
1357
+ screenRows: rowNotes.map(() => chromeRow)
1358
+ };
1359
+ }
1360
+ function anchoredExtrasFor(rowIndex, options, width, truecolor) {
1361
+ const { notes, focusedNote, mode, draft, selection } = options;
1362
+ const noteEntries = anchoredNoteRowsFor(rowIndex, notes, focusedNote, width, truecolor);
1363
+ const showDraft = mode === "note" && draftAnchorRowFor(selection) === rowIndex;
1364
+ if (!showDraft) {
1365
+ return noteEntries;
1366
+ }
1367
+ const chromeRow = { kind: "chrome", index: null };
1368
+ return {
1369
+ lines: [...noteEntries.lines, paintAnchoredDraftRow(draft, width, truecolor)],
1370
+ screenRows: [...noteEntries.screenRows, chromeRow]
1371
+ };
1372
+ }
1373
+ function paintConfirmSummary(noteCount, width, truecolor) {
1374
+ const text = `${noteCount} notes are not sent.`.slice(0, width);
1375
+ return padPanelLine(fg(theme.chrome, truecolor) + text + DEFAULT_FG, text.length, width);
1376
+ }
1377
+ function paintConfirmChoices(width, truecolor) {
1378
+ const choices = [
1379
+ ["s", " send "],
1380
+ ["d", " discard and apply the edit "],
1381
+ ["esc", " back"]
1382
+ ];
1383
+ const content = choices.map(([key, label]) => bg(theme.chrome, truecolor) + key + DEFAULT_BG + label).join("");
1384
+ const plainLength = choices.reduce((sum, [key, label]) => sum + key.length + label.length, 0);
1385
+ return padPanelLine(content, plainLength, width);
1386
+ }
1387
+ function buildPanel(options, width, truecolor) {
1388
+ const { notes, focusedNote, mode, draft, notePosition } = options;
1389
+ const height = panelHeight(notes.length, mode, notePosition);
1390
+ if (height === 0) {
1391
+ return { lines: [], screenRows: [] };
1392
+ }
1393
+ const chromeRow = { kind: "chrome", index: null };
1394
+ if (mode === "confirm") {
1395
+ return {
1396
+ lines: [
1397
+ paintConfirmSummary(notes.length, width, truecolor),
1398
+ paintConfirmChoices(width, truecolor)
1399
+ ],
1400
+ screenRows: [chromeRow, chromeRow]
1401
+ };
1402
+ }
1403
+ const draftRows = mode === "note" ? PANEL_DRAFT_ROWS : 0;
1404
+ const availableForNotes = Math.max(0, height - PANEL_TITLE_ROWS - draftRows);
1405
+ const visibleNotes = notes.slice(0, availableForNotes);
1406
+ const lines = [
1407
+ paintPanelTitle(notes.length, width, truecolor),
1408
+ ...visibleNotes.map((note) => paintNoteRow(note, note.id === focusedNote, width, truecolor)),
1409
+ ...mode === "note" ? [paintDraftRow(draft, width, truecolor)] : []
1410
+ ];
1411
+ return { lines, screenRows: lines.map(() => chromeRow) };
1412
+ }
1413
+ function takeBodyEntries(entries, bodyRows) {
1414
+ const lines = [];
1415
+ const screenRows = [];
1416
+ let count = 0;
1417
+ for (const entry of entries) {
1418
+ if (count > 0 && lines.length + entry.lines.length > bodyRows) {
1419
+ break;
1420
+ }
1421
+ lines.push(...entry.lines);
1422
+ screenRows.push(...entry.screenRows);
1423
+ count += 1;
1424
+ if (lines.length >= bodyRows) {
1425
+ break;
1426
+ }
1427
+ }
1428
+ return { lines: lines.slice(0, bodyRows), screenRows: screenRows.slice(0, bodyRows), count };
1429
+ }
1430
+ function assembleScreen(header, rule, bodyLines, bodyScreenRows, bodyRows, panelLines, panelScreenRows, statusMessage, width, height, truecolor, panes, lastRow) {
1431
+ const padCount = Math.max(0, bodyRows - bodyLines.length);
1432
+ const padLines = Array.from({ length: padCount }, () => paintBlankLine(width));
1433
+ const padScreenRows = Array.from({ length: padCount }, () => ({
1434
+ kind: "chrome",
1435
+ index: null
1436
+ }));
1437
+ const lines = [
1438
+ header,
1439
+ rule,
1440
+ ...bodyLines,
1441
+ ...padLines,
1442
+ ...panelLines,
1443
+ paintStatus(width, truecolor, statusMessage)
1444
+ ].slice(0, height);
1445
+ const allRows = [
1446
+ { kind: "chrome", index: null },
1447
+ { kind: "chrome", index: null },
1448
+ ...bodyScreenRows,
1449
+ ...padScreenRows,
1450
+ ...panelScreenRows,
1451
+ { kind: "chrome", index: null }
1452
+ ];
1453
+ const rows = allRows.slice(0, height);
1454
+ const map = { rows, panes };
1455
+ return { lines, map, lastRow };
1456
+ }
1457
+ function paintSplit(options) {
1458
+ const {
1459
+ model,
1460
+ width,
1461
+ height,
1462
+ path,
1463
+ tokens,
1464
+ truecolor,
1465
+ rowBand,
1466
+ scrollTop,
1467
+ selection,
1468
+ notes,
1469
+ mode,
1470
+ notePosition
1471
+ } = options;
1472
+ const numberWidth = computeNumberWidth(model);
1473
+ const changeBackground = hasRemovals(model);
1474
+ const hasLeftMarkerColumn = hasPaneNotes(notes, "left");
1475
+ const hasRightMarkerColumn = hasPaneNotes(notes, "right");
1476
+ const leftMarkerWidth = hasLeftMarkerColumn ? MARKER_WIDTH : 0;
1477
+ const rightMarkerWidth = hasRightMarkerColumn ? MARKER_WIDTH : 0;
1478
+ const fixedWidth = PANE_COUNT * (numberWidth + GUTTER_SPACE_WIDTH + SIGN_BAR_WIDTH) + DIVIDER_WIDTH + leftMarkerWidth + rightMarkerWidth;
1479
+ const remaining = Math.max(0, width - fixedWidth);
1480
+ const leftPaneWidth = Math.floor(remaining / PANE_COUNT);
1481
+ const rightPaneWidth = remaining - leftPaneWidth;
1482
+ const leftTextStart = numberWidth + GUTTER_SPACE_WIDTH + SIGN_BAR_WIDTH + leftMarkerWidth;
1483
+ const leftTextEnd = leftTextStart + leftPaneWidth;
1484
+ const rightGutterStart = leftTextEnd + DIVIDER_WIDTH;
1485
+ const rightTextStart = rightGutterStart + numberWidth + GUTTER_SPACE_WIDTH + SIGN_BAR_WIDTH + rightMarkerWidth;
1486
+ const rightTextEnd = rightTextStart + rightPaneWidth;
1487
+ const leftBounds = {
1488
+ pane: "left",
1489
+ gutterStart: 0,
1490
+ textStart: leftTextStart,
1491
+ textEnd: leftTextEnd
1492
+ };
1493
+ const rightBounds = {
1494
+ pane: "right",
1495
+ gutterStart: rightGutterStart,
1496
+ textStart: rightTextStart,
1497
+ textEnd: rightTextEnd
1498
+ };
1499
+ const counts = changeCounts(model);
1500
+ const bodyRows = bodyHeight(height, notes.length, mode, notePosition);
1501
+ const visible = visibleRows(model).slice(scrollTop, scrollTop + bodyRows);
1502
+ const bodyEntries = visible.map((entry, offset) => {
1503
+ const cursorRow = scrollTop + offset === model.cursor;
1504
+ if (entry.kind === "fold") {
1505
+ return {
1506
+ lines: [paintFoldRow(lookupFold(model, entry.foldIndex), width, truecolor, cursorRow)],
1507
+ screenRows: [{ kind: "fold", index: entry.foldIndex }]
1508
+ };
1509
+ }
1510
+ const line = paintModelRow(
1511
+ lookupRow(model, entry.index),
1512
+ entry.index,
1513
+ leftBounds,
1514
+ rightBounds,
1515
+ numberWidth,
1516
+ tokens,
1517
+ rowBand,
1518
+ changeBackground,
1519
+ truecolor,
1520
+ selection,
1521
+ notes,
1522
+ hasLeftMarkerColumn,
1523
+ hasRightMarkerColumn,
1524
+ cursorRow
1525
+ );
1526
+ const screenRow = { kind: "row", index: entry.index };
1527
+ const extras = notePosition === "anchored" ? anchoredExtrasFor(entry.index, options, width, truecolor) : { lines: [], screenRows: [] };
1528
+ return { lines: [line, ...extras.lines], screenRows: [screenRow, ...extras.screenRows] };
1529
+ });
1530
+ const fill = takeBodyEntries(bodyEntries, bodyRows);
1531
+ const panel = buildPanel(options, width, truecolor);
1532
+ return assembleScreen(
1533
+ paintHeader(path, counts.add, counts.del, width, truecolor),
1534
+ paintRule(width, truecolor),
1535
+ fill.lines,
1536
+ fill.screenRows,
1537
+ bodyRows,
1538
+ panel.lines,
1539
+ panel.screenRows,
1540
+ null,
1541
+ width,
1542
+ height,
1543
+ truecolor,
1544
+ [leftBounds, rightBounds],
1545
+ scrollTop + Math.max(fill.count, 1) - 1
1546
+ );
1547
+ }
1548
+ var UNIFIED_SIGN_BAR = {
1549
+ context: { char: " ", color: null },
1550
+ add: { char: "\u258C", color: theme.addBar },
1551
+ del: { char: "\u258C", color: theme.delBar }
1552
+ };
1553
+ function paintUnifiedHalf(halfKind, lineNumber, text, tokens, spans, changeColor, numberWidth, textWidth, rowBand, truecolor, highlightSpans, hasMarkerColumn, annotated, cursorRow) {
1554
+ const bar = UNIFIED_SIGN_BAR[halfKind];
1555
+ const rendered = renderPaneText(
1556
+ text,
1557
+ tokens,
1558
+ spans,
1559
+ changeColor,
1560
+ textWidth,
1561
+ rowBand,
1562
+ truecolor,
1563
+ highlightSpans
1564
+ );
1565
+ return paintGutter(lineNumber, numberWidth, truecolor, cursorRow) + paintSignBar(bar.char, bar.color, truecolor) + paintMarkerColumn(hasMarkerColumn, annotated, truecolor) + " ".repeat(TEXT_GAP_WIDTH) + rendered + RESET;
1566
+ }
1567
+ function paintUnifiedBodyEntry(entry, model, numberWidth, textWidth, width, tokens, rowBand, changeBackground, truecolor, selection, notes, hasMarkerColumn, cursorRow) {
1568
+ if (entry.kind === "fold") {
1569
+ return {
1570
+ lines: [paintFoldRow(lookupFold(model, entry.foldIndex), width, truecolor, cursorRow)],
1571
+ screenRows: [{ kind: "fold", index: entry.foldIndex }]
1572
+ };
1573
+ }
1574
+ const row = lookupRow(model, entry.index);
1575
+ const spans = row.kind === "context" ? NO_SPANS : changedSpans(row.left, row.right);
1576
+ const screenRow = { kind: "row", index: entry.index };
1577
+ const hasSelection = selection !== null && selection.pane === "right";
1578
+ const addColor = changeBackground ? theme.addSpan : null;
1579
+ const delColor = changeBackground ? theme.delSpan : null;
1580
+ if (row.kind === "context") {
1581
+ const selectionSpan = hasSelection ? selectionSpanFor(selection, entry.index, row.right.length) : null;
1582
+ const highlights = [
1583
+ ...selectionSpan === null ? [] : [selectionSpan],
1584
+ ...noteSpansFor(notes, entry.index, "right", row.right.length)
1585
+ ];
1586
+ const line = paintUnifiedHalf(
1587
+ "context",
1588
+ row.rightNumber,
1589
+ row.right,
1590
+ tokens(row.right, row.rightNumber),
1591
+ [],
1592
+ null,
1593
+ numberWidth,
1594
+ textWidth,
1595
+ rowBand,
1596
+ truecolor,
1597
+ highlights,
1598
+ hasMarkerColumn,
1599
+ isAnnotatedRow(notes, entry.index, "right"),
1600
+ cursorRow
1601
+ );
1602
+ return { lines: [line], screenRows: [screenRow] };
1603
+ }
1604
+ if (row.kind === "add") {
1605
+ const selectionSpan = hasSelection ? selectionSpanFor(selection, entry.index, row.right.length) : null;
1606
+ const highlights = [
1607
+ ...selectionSpan === null ? [] : [selectionSpan],
1608
+ ...noteSpansFor(notes, entry.index, "right", row.right.length)
1609
+ ];
1610
+ const line = paintUnifiedHalf(
1611
+ "add",
1612
+ row.rightNumber,
1613
+ row.right,
1614
+ tokens(row.right, row.rightNumber),
1615
+ spans.right,
1616
+ addColor,
1617
+ numberWidth,
1618
+ textWidth,
1619
+ rowBand,
1620
+ truecolor,
1621
+ highlights,
1622
+ hasMarkerColumn,
1623
+ isAnnotatedRow(notes, entry.index, "right"),
1624
+ cursorRow
1625
+ );
1626
+ return { lines: [line], screenRows: [screenRow] };
1627
+ }
1628
+ if (row.kind === "del") {
1629
+ const selectionSpan = hasSelection ? selectionSpanFor(selection, entry.index, row.left.length) : null;
1630
+ const highlights = [
1631
+ ...selectionSpan === null ? [] : [selectionSpan],
1632
+ ...noteSpansFor(notes, entry.index, "left", row.left.length)
1633
+ ];
1634
+ const line = paintUnifiedHalf(
1635
+ "del",
1636
+ row.leftNumber,
1637
+ row.left,
1638
+ tokens(row.left, row.leftNumber),
1639
+ spans.left,
1640
+ delColor,
1641
+ numberWidth,
1642
+ textWidth,
1643
+ rowBand,
1644
+ truecolor,
1645
+ highlights,
1646
+ hasMarkerColumn,
1647
+ isAnnotatedRow(notes, entry.index, "left"),
1648
+ cursorRow
1649
+ );
1650
+ return { lines: [line], screenRows: [screenRow] };
1651
+ }
1652
+ const delSelectionSpan = hasSelection ? selectionSpanFor(selection, entry.index, row.left.length) : null;
1653
+ const addSelectionSpan = hasSelection ? selectionSpanFor(selection, entry.index, row.right.length) : null;
1654
+ const delHighlights = [
1655
+ ...delSelectionSpan === null ? [] : [delSelectionSpan],
1656
+ ...noteSpansFor(notes, entry.index, "left", row.left.length)
1657
+ ];
1658
+ const addHighlights = [
1659
+ ...addSelectionSpan === null ? [] : [addSelectionSpan],
1660
+ ...noteSpansFor(notes, entry.index, "right", row.right.length)
1661
+ ];
1662
+ const delLine = paintUnifiedHalf(
1663
+ "del",
1664
+ row.leftNumber,
1665
+ row.left,
1666
+ tokens(row.left, row.leftNumber),
1667
+ spans.left,
1668
+ delColor,
1669
+ numberWidth,
1670
+ textWidth,
1671
+ rowBand,
1672
+ truecolor,
1673
+ delHighlights,
1674
+ hasMarkerColumn,
1675
+ isAnnotatedRow(notes, entry.index, "left"),
1676
+ cursorRow
1677
+ );
1678
+ const addLine = paintUnifiedHalf(
1679
+ "add",
1680
+ row.rightNumber,
1681
+ row.right,
1682
+ tokens(row.right, row.rightNumber),
1683
+ spans.right,
1684
+ addColor,
1685
+ numberWidth,
1686
+ textWidth,
1687
+ rowBand,
1688
+ truecolor,
1689
+ addHighlights,
1690
+ hasMarkerColumn,
1691
+ isAnnotatedRow(notes, entry.index, "right"),
1692
+ cursorRow
1693
+ );
1694
+ return { lines: [delLine, addLine], screenRows: [screenRow, screenRow] };
1695
+ }
1696
+ function paintUnified(options) {
1697
+ const {
1698
+ model,
1699
+ width,
1700
+ height,
1701
+ path,
1702
+ tokens,
1703
+ truecolor,
1704
+ rowBand,
1705
+ scrollTop,
1706
+ selection,
1707
+ notes,
1708
+ mode,
1709
+ notePosition
1710
+ } = options;
1711
+ const numberWidth = computeNumberWidth(model);
1712
+ const changeBackground = hasRemovals(model);
1713
+ const hasMarkerColumn = notes.length > 0;
1714
+ const markerWidth = hasMarkerColumn ? MARKER_WIDTH : 0;
1715
+ const fixedWidth = UNIFIED_PANE_COUNT * (numberWidth + GUTTER_SPACE_WIDTH + SIGN_BAR_WIDTH) + markerWidth + TEXT_GAP_WIDTH;
1716
+ const textStart = fixedWidth;
1717
+ const textWidth = Math.max(0, width - fixedWidth);
1718
+ const textEnd = textStart + textWidth;
1719
+ const rightBounds = { pane: "right", gutterStart: 0, textStart, textEnd };
1720
+ const counts = changeCounts(model);
1721
+ const bodyRows = bodyHeight(height, notes.length, mode, notePosition);
1722
+ const visible = visibleRows(model).slice(scrollTop, scrollTop + bodyRows);
1723
+ const entries = visible.map((entry, offset) => {
1724
+ const cursorRow = scrollTop + offset === model.cursor;
1725
+ const base = paintUnifiedBodyEntry(
1726
+ entry,
1727
+ model,
1728
+ numberWidth,
1729
+ textWidth,
1730
+ width,
1731
+ tokens,
1732
+ rowBand,
1733
+ changeBackground,
1734
+ truecolor,
1735
+ selection,
1736
+ notes,
1737
+ hasMarkerColumn,
1738
+ cursorRow
1739
+ );
1740
+ if (notePosition !== "anchored" || entry.kind === "fold") {
1741
+ return base;
1742
+ }
1743
+ const extras = anchoredExtrasFor(entry.index, options, width, truecolor);
1744
+ return {
1745
+ lines: [...base.lines, ...extras.lines],
1746
+ screenRows: [...base.screenRows, ...extras.screenRows]
1747
+ };
1748
+ });
1749
+ const fill = takeBodyEntries(entries, bodyRows);
1750
+ const statusMessage = layoutStatusMessage(options);
1751
+ const panel = buildPanel(options, width, truecolor);
1752
+ return assembleScreen(
1753
+ paintHeader(path, counts.add, counts.del, width, truecolor),
1754
+ paintRule(width, truecolor),
1755
+ fill.lines,
1756
+ fill.screenRows,
1757
+ bodyRows,
1758
+ panel.lines,
1759
+ panel.screenRows,
1760
+ statusMessage,
1761
+ width,
1762
+ height,
1763
+ truecolor,
1764
+ [rightBounds],
1765
+ scrollTop + Math.max(fill.count, 1) - 1
1766
+ );
1767
+ }
1768
+
1769
+ // src/tui/paint/paint.ts
1770
+ var SPAN_SIMILARITY_FLOOR = 0.3;
1771
+ var MIN_SPLIT_WIDTH = 90;
1772
+ var NEW_FILE_REASON = "whole file is new \xB7 unified";
1773
+ var NARROW_REASON = "narrow \xB7 unified";
1774
+ var noTokens = () => [];
1775
+ function hasRemovals(model) {
1776
+ return model.rows.some((row) => row.kind === "del" || row.kind === "replace");
1777
+ }
1778
+ function indentWidth(line) {
1779
+ return line.length - line.trimStart().length;
1780
+ }
1781
+ function changedSpans(before, after) {
1782
+ const chunks = diffWordsWithSpace(before, after);
1783
+ const scan = chunks.reduce(
1784
+ (state, chunk) => {
1785
+ const length = chunk.value.length;
1786
+ if (chunk.removed === true) {
1787
+ return {
1788
+ ...state,
1789
+ left: [...state.left, { start: state.leftCursor, end: state.leftCursor + length }],
1790
+ leftCursor: state.leftCursor + length
1791
+ };
1792
+ }
1793
+ if (chunk.added === true) {
1794
+ return {
1795
+ ...state,
1796
+ right: [...state.right, { start: state.rightCursor, end: state.rightCursor + length }],
1797
+ rightCursor: state.rightCursor + length
1798
+ };
1799
+ }
1800
+ return {
1801
+ ...state,
1802
+ sharedLength: state.sharedLength + length,
1803
+ leftCursor: state.leftCursor + length,
1804
+ rightCursor: state.rightCursor + length
1805
+ };
1806
+ },
1807
+ { left: [], right: [], sharedLength: 0, leftCursor: 0, rightCursor: 0 }
1808
+ );
1809
+ const { left, right, sharedLength } = scan;
1810
+ const indent = Math.min(indentWidth(before), indentWidth(after));
1811
+ const longer = Math.max(before.length, after.length) - indent;
1812
+ const sharedFraction = longer <= 0 ? 1 : (sharedLength - indent) / longer;
1813
+ if (sharedFraction < SPAN_SIMILARITY_FLOOR) {
1814
+ return {
1815
+ left: before === "" ? [] : [{ start: 0, end: before.length }],
1816
+ right: after === "" ? [] : [{ start: 0, end: after.length }]
1817
+ };
1818
+ }
1819
+ return { left, right };
1820
+ }
1821
+ function decideLayout(options) {
1822
+ const preferred = options.layout;
1823
+ const forcedReason = !hasRemovals(options.model) ? NEW_FILE_REASON : options.width < MIN_SPLIT_WIDTH ? NARROW_REASON : null;
1824
+ if (forcedReason === null) {
1825
+ return { layout: preferred, overrideReason: null };
1826
+ }
1827
+ return { layout: "unified", overrideReason: preferred === "split" ? forcedReason : null };
1828
+ }
1829
+ function layoutStatusMessage(options) {
1830
+ return decideLayout(options).overrideReason;
1831
+ }
1832
+ function paint(options) {
1833
+ const { layout } = decideLayout(options);
1834
+ return layout === "unified" ? paintUnified(options) : paintSplit(options);
1835
+ }
1836
+
1837
+ // src/editors/languages.ts
1838
+ import { extname } from "node:path";
1839
+ var LANGS = {
1840
+ ".go": "go",
1841
+ ".rb": "ruby",
1842
+ ".rake": "ruby",
1843
+ ".ts": "typescript",
1844
+ ".tsx": "typescript",
1845
+ ".js": "javascript",
1846
+ ".jsx": "javascript",
1847
+ ".mjs": "javascript",
1848
+ ".py": "python3",
1849
+ ".ex": "elixir",
1850
+ ".exs": "elixir",
1851
+ ".rs": "rust",
1852
+ ".sh": "sh",
1853
+ ".bash": "sh",
1854
+ ".fish": "fish",
1855
+ ".zsh": "zsh",
1856
+ ".sql": "sql",
1857
+ ".json": "json",
1858
+ ".tf": "terraform",
1859
+ ".proto": "proto",
1860
+ ".dockerfile": "dockerfile",
1861
+ ".toml": "toml",
1862
+ ".yaml": "yaml",
1863
+ ".yml": "yaml",
1864
+ ".md": "markdown",
1865
+ ".css": "css",
1866
+ ".html": "html",
1867
+ ".erb": "html",
1868
+ ".lua": "lua",
1869
+ ".c": "c",
1870
+ ".h": "c"
1871
+ };
1872
+ function syntaxName(sourcePath) {
1873
+ const ext = extname(sourcePath).toLowerCase();
1874
+ return LANGS[ext] ?? null;
1875
+ }
1876
+ var SHIKI_TRANSLATIONS = {
1877
+ python3: "python",
1878
+ sh: "shellscript",
1879
+ zsh: "shellscript",
1880
+ fish: "fish",
1881
+ proto: "proto",
1882
+ dockerfile: "docker",
1883
+ terraform: "terraform"
1884
+ };
1885
+ function shikiLanguage(sourcePath) {
1886
+ const name = syntaxName(sourcePath);
1887
+ if (name === null) {
1888
+ return null;
1889
+ }
1890
+ return SHIKI_TRANSLATIONS[name] ?? name;
1891
+ }
1892
+
1893
+ // src/tui/syntax/syntax.ts
1894
+ var THEME_ID = "github-dark";
1895
+ var MAX_CACHED_LINES = 4096;
1896
+ var providers = /* @__PURE__ */ new Map();
1897
+ function isBundledLanguage(lang, bundled) {
1898
+ return lang in bundled;
1899
+ }
1900
+ function isBundledTheme(id, bundled) {
1901
+ return id in bundled;
1902
+ }
1903
+ var loadShikiHighlighter = async (lang, theme2) => {
1904
+ const shiki = await import("shiki");
1905
+ if (!isBundledLanguage(lang, shiki.bundledLanguages) || !isBundledTheme(theme2, shiki.bundledThemes)) {
1906
+ throw new Error(`shiki does not bundle language "${lang}" or theme "${theme2}"`);
1907
+ }
1908
+ const highlighter = await shiki.createHighlighter({ langs: [lang], themes: [theme2] });
1909
+ return {
1910
+ codeToTokensBase(code, options) {
1911
+ if (!isBundledLanguage(options.lang, shiki.bundledLanguages) || !isBundledTheme(options.theme, shiki.bundledThemes)) {
1912
+ return [[]];
1913
+ }
1914
+ return highlighter.codeToTokensBase(code, { lang: options.lang, theme: options.theme });
1915
+ }
1916
+ };
1917
+ };
1918
+ function hasColor(token) {
1919
+ return typeof token.color === "string";
1920
+ }
1921
+ function tokenizeLine(highlighter, lang, line) {
1922
+ const tokenizedLines = highlighter.codeToTokensBase(line, { lang, theme: THEME_ID });
1923
+ const lineTokens = tokenizedLines[0] ?? [];
1924
+ return lineTokens.filter(hasColor).map((token) => ({
1925
+ start: token.offset,
1926
+ end: token.offset + token.content.length,
1927
+ color: token.color
1928
+ }));
1929
+ }
1930
+ async function tryLoadHighlighter(load, lang, theme2) {
1931
+ try {
1932
+ return await load(lang, theme2);
1933
+ } catch {
1934
+ return null;
1935
+ }
1936
+ }
1937
+ function remember(cache, line, tokens) {
1938
+ if (cache.size >= MAX_CACHED_LINES) {
1939
+ const oldest = cache.keys().next();
1940
+ if (oldest.done !== true) {
1941
+ cache.delete(oldest.value);
1942
+ }
1943
+ }
1944
+ cache.set(line, tokens);
1945
+ }
1946
+ async function buildTokenProvider(load, lang) {
1947
+ const highlighter = await tryLoadHighlighter(load, lang, THEME_ID);
1948
+ if (highlighter === null) {
1949
+ return noTokens;
1950
+ }
1951
+ const cache = /* @__PURE__ */ new Map();
1952
+ return (line) => {
1953
+ const cached = cache.get(line);
1954
+ if (cached !== void 0) {
1955
+ return cached;
1956
+ }
1957
+ const tokens = tokenizeLine(highlighter, lang, line);
1958
+ remember(cache, line, tokens);
1959
+ return tokens;
1960
+ };
1961
+ }
1962
+ async function createTokenProvider(options, loadHighlighter = loadShikiHighlighter) {
1963
+ if (!options.enabled) {
1964
+ return noTokens;
1965
+ }
1966
+ const lang = shikiLanguage(options.path);
1967
+ if (lang === null) {
1968
+ return noTokens;
1969
+ }
1970
+ if (loadHighlighter !== loadShikiHighlighter) {
1971
+ return buildTokenProvider(loadHighlighter, lang);
1972
+ }
1973
+ const shared = providers.get(lang);
1974
+ if (shared !== void 0) {
1975
+ return shared;
1976
+ }
1977
+ const created = buildTokenProvider(loadHighlighter, lang);
1978
+ providers.set(lang, created);
1979
+ return created;
1980
+ }
1981
+
1982
+ // src/tui/input/keys.ts
1983
+ var ESC = "\x1B";
1984
+ var CSI_FINAL_START = 64;
1985
+ var CSI_FINAL_END = 126;
1986
+ var CSI_PARAM_RUN = /^[\x20-\x3f]*/;
1987
+ var ARROW_NAMES = {
1988
+ A: "up",
1989
+ B: "down",
1990
+ C: "right",
1991
+ D: "left"
1992
+ };
1993
+ var CTRL_NAMES = {
1994
+ "": "d",
1995
+ "": "u",
1996
+ "": "s",
1997
+ "": "q",
1998
+ "": "c"
1999
+ };
2000
+ function consumeMouseSequence(chunk, start) {
2001
+ const closeIndex = chunk.slice(start).search(/[Mm]/);
2002
+ if (closeIndex === -1) {
2003
+ return chunk.length;
2004
+ }
2005
+ return start + closeIndex + 1;
2006
+ }
2007
+ function consumeUnknownCsi(chunk, start) {
2008
+ const params = CSI_PARAM_RUN.exec(chunk.slice(start))?.[0] ?? "";
2009
+ const afterParams = start + params.length;
2010
+ const code = chunk.charCodeAt(afterParams);
2011
+ return code >= CSI_FINAL_START && code <= CSI_FINAL_END ? afterParams + 1 : afterParams;
2012
+ }
2013
+ function parseEscape(chunk, index) {
2014
+ const afterEsc = index + 1;
2015
+ if (afterEsc >= chunk.length || chunk[afterEsc] !== "[") {
2016
+ return { event: { name: "escape", ctrl: false, text: "" }, next: afterEsc };
2017
+ }
2018
+ const afterBracket = afterEsc + 1;
2019
+ const marker = afterBracket < chunk.length ? chunk[afterBracket] : void 0;
2020
+ if (marker === "<") {
2021
+ return { event: null, next: consumeMouseSequence(chunk, afterBracket + 1) };
2022
+ }
2023
+ if (marker !== void 0 && marker in ARROW_NAMES) {
2024
+ return { event: { name: ARROW_NAMES[marker], ctrl: false, text: "" }, next: afterBracket + 1 };
2025
+ }
2026
+ return { event: null, next: consumeUnknownCsi(chunk, afterBracket) };
2027
+ }
2028
+ function parseSingle(chunk, index) {
2029
+ const char = chunk[index];
2030
+ const next = index + 1;
2031
+ if (char === "\r" || char === "\n") {
2032
+ return { event: { name: "enter", ctrl: false, text: "" }, next };
2033
+ }
2034
+ if (char === "\x7F" || char === "\b") {
2035
+ return { event: { name: "backspace", ctrl: false, text: "" }, next };
2036
+ }
2037
+ if (char === " ") {
2038
+ return { event: { name: "tab", ctrl: false, text: "" }, next };
2039
+ }
2040
+ const ctrlName = CTRL_NAMES[char];
2041
+ if (ctrlName !== void 0) {
2042
+ return { event: { name: ctrlName, ctrl: true, text: "" }, next };
2043
+ }
2044
+ return { event: { name: char, ctrl: false, text: char }, next };
2045
+ }
2046
+ function parseKeys(chunk) {
2047
+ const events = [];
2048
+ let index = 0;
2049
+ while (index < chunk.length) {
2050
+ if (chunk[index] === ESC) {
2051
+ const result2 = parseEscape(chunk, index);
2052
+ if (result2.event !== null) {
2053
+ events.push(result2.event);
2054
+ }
2055
+ index = result2.next;
2056
+ continue;
2057
+ }
2058
+ const result = parseSingle(chunk, index);
2059
+ events.push(result.event);
2060
+ index = result.next;
2061
+ }
2062
+ return events;
2063
+ }
2064
+
2065
+ // src/tui/input/mouse.ts
2066
+ var MOUSE_ON = "\x1B[?1000h\x1B[?1002h\x1B[?1006h";
2067
+ var MOUSE_OFF = "\x1B[?1006l\x1B[?1002l\x1B[?1000l";
2068
+ var MOUSE_REPORT_SOURCE = "\\x1b\\[<(\\d+);(\\d+);(\\d+)([Mm])";
2069
+ var BUTTON_MASK = 3;
2070
+ var DRAG_BIT = 32;
2071
+ var SCROLL_BIT = 64;
2072
+ var SHIFT_BIT = 4;
2073
+ function mouseReportPattern() {
2074
+ return new RegExp(MOUSE_REPORT_SOURCE, "g");
2075
+ }
2076
+ function toMouseEvent(match) {
2077
+ const word = Number(match[1]);
2078
+ const column = Number(match[2]);
2079
+ const row = Number(match[3]);
2080
+ const isRelease = match[4] === "m";
2081
+ const kind = isRelease ? "up" : (word & SCROLL_BIT) !== 0 ? "scroll" : (word & DRAG_BIT) !== 0 ? "drag" : "down";
2082
+ return {
2083
+ kind,
2084
+ button: word & BUTTON_MASK,
2085
+ row,
2086
+ column,
2087
+ shift: (word & SHIFT_BIT) !== 0
2088
+ };
2089
+ }
2090
+ function splitInput(chunk) {
2091
+ const matches = [...chunk.matchAll(mouseReportPattern())];
2092
+ const segmentStarts = [0, ...matches.map((match) => match.index + match[0].length)];
2093
+ const keys = segmentStarts.map((start, position) => chunk.slice(start, matches[position]?.index)).join("");
2094
+ return { keys, mouse: matches.map(toMouseEvent) };
2095
+ }
2096
+
2097
+ // src/tui/notes/notes.ts
2098
+ import { writeFileSync } from "node:fs";
2099
+ function rangeOf(selection) {
2100
+ const reversed = selection.anchorRow > selection.headRow || selection.anchorRow === selection.headRow && selection.anchorColumn > selection.headColumn;
2101
+ if (!reversed) {
2102
+ return {
2103
+ startRow: selection.anchorRow,
2104
+ endRow: selection.headRow,
2105
+ pane: selection.pane,
2106
+ startColumn: selection.anchorColumn,
2107
+ endColumn: selection.headColumn + 1
2108
+ };
2109
+ }
2110
+ return {
2111
+ startRow: selection.headRow,
2112
+ endRow: selection.anchorRow,
2113
+ pane: selection.pane,
2114
+ startColumn: selection.headColumn,
2115
+ endColumn: selection.anchorColumn + 1
2116
+ };
2117
+ }
2118
+ function noteFromSelection(model, selection, id, text) {
2119
+ const trimmed = text.trim();
2120
+ if (trimmed === "") {
2121
+ return null;
2122
+ }
2123
+ const range = rangeOf(selection);
2124
+ const startRow = model.rows[range.startRow];
2125
+ const endRow = model.rows[range.endRow];
2126
+ if (startRow === void 0 || endRow === void 0) {
2127
+ return null;
2128
+ }
2129
+ const line = range.pane === "right" ? startRow.rightNumber : startRow.leftNumber;
2130
+ const endLine = range.pane === "right" ? endRow.rightNumber : endRow.leftNumber;
2131
+ const code = range.pane === "right" ? startRow.right : startRow.left;
2132
+ return {
2133
+ id,
2134
+ rowIndex: range.startRow,
2135
+ endRowIndex: range.endRow,
2136
+ pane: range.pane,
2137
+ startColumn: range.startColumn,
2138
+ endColumn: range.endColumn,
2139
+ line,
2140
+ endLine,
2141
+ code,
2142
+ text: trimmed
2143
+ };
2144
+ }
2145
+ function firstRowEndColumn(note) {
2146
+ return note.endRowIndex > note.rowIndex ? note.code.length : note.endColumn;
2147
+ }
2148
+ function isWholeLine(note) {
2149
+ return note.startColumn === 0 && firstRowEndColumn(note) >= note.code.length;
2150
+ }
2151
+ function withSpanSuffix(note) {
2152
+ const selected = note.code.slice(note.startColumn, firstRowEndColumn(note));
2153
+ return `${note.text} [re: "${selected}"]`;
2154
+ }
2155
+ function sortNotes(notes) {
2156
+ return [...notes].sort(
2157
+ (first, second) => first.rowIndex === second.rowIndex ? first.startColumn - second.startColumn : first.rowIndex - second.rowIndex
2158
+ );
2159
+ }
2160
+ function toQuestions(notes) {
2161
+ return sortNotes(notes).map((note) => ({
2162
+ line: note.line,
2163
+ code: note.code,
2164
+ text: isWholeLine(note) ? note.text : withSpanSuffix(note)
2165
+ }));
2166
+ }
2167
+ function writeResult(path, notes) {
2168
+ try {
2169
+ writeFileSync(path, JSON.stringify({ questions: toQuestions(notes) }, null, 2), "utf-8");
2170
+ } catch {
2171
+ return;
2172
+ }
2173
+ }
2174
+
2175
+ // src/tui/tui.ts
2176
+ var NOTE_PANE = "right";
2177
+ function geometryFor(state, model, height, width) {
2178
+ return {
2179
+ model,
2180
+ layout: state.layout,
2181
+ width,
2182
+ height,
2183
+ notes: state.notes,
2184
+ mode: state.mode,
2185
+ notePosition: state.notePosition,
2186
+ selection: state.selection
2187
+ };
2188
+ }
2189
+ function pageSize(state, height, width) {
2190
+ return pageRowStep(geometryFor(state, state.model, height, width), state.scrollTop);
2191
+ }
2192
+ function visibleIndexForRow2(model, rowIndex) {
2193
+ const visible = visibleRows(model);
2194
+ const found = visible.findIndex((entry) => entry.kind === "row" && entry.index === rowIndex);
2195
+ return found === -1 ? model.cursor : found;
2196
+ }
2197
+ function isChangedEntry(model, entry) {
2198
+ return entry.kind === "row" && model.rows[entry.index].kind !== "context";
2199
+ }
2200
+ function walkFrom(from, length, direction) {
2201
+ const count = direction === 1 ? length - from : from + 1;
2202
+ return Array.from({ length: Math.max(0, count) }, (_, step) => from + step * direction);
2203
+ }
2204
+ function jumpToRun(model, direction) {
2205
+ const visible = visibleRows(model);
2206
+ if (visible.length === 0) {
2207
+ return model;
2208
+ }
2209
+ const walk = walkFrom(model.cursor, visible.length, direction);
2210
+ const current = visible[model.cursor];
2211
+ const onRun = current !== void 0 && isChangedEntry(model, current);
2212
+ const runEnd = walk.findIndex((index) => !isChangedEntry(model, visible[index]));
2213
+ const rest = onRun ? runEnd === -1 ? [] : walk.slice(runEnd) : walk.slice(1);
2214
+ const target = rest.find((index) => isChangedEntry(model, visible[index]));
2215
+ return target === void 0 ? model : { ...model, cursor: target };
2216
+ }
2217
+ function toggleFoldAtCursor(model) {
2218
+ const visible = visibleRows(model);
2219
+ const entry = visible[model.cursor];
2220
+ if (entry === void 0 || entry.kind !== "fold") {
2221
+ return model;
2222
+ }
2223
+ return toggleFold(model, entry.foldIndex);
2224
+ }
2225
+ var WHEEL_UP_BUTTON = 0;
2226
+ var WHEEL_DOWN_BUTTON = 1;
2227
+ var WHEEL_ROWS = 3;
2228
+ function wheelStep(button) {
2229
+ if (button === WHEEL_UP_BUTTON) {
2230
+ return -WHEEL_ROWS;
2231
+ }
2232
+ return button === WHEEL_DOWN_BUTTON ? WHEEL_ROWS : 0;
2233
+ }
2234
+ function applyScroll(state, event, height, width) {
2235
+ const step = wheelStep(event.button);
2236
+ if (step === 0) {
2237
+ return state;
2238
+ }
2239
+ const maxScroll = maxScrollTop(geometryFor(state, state.model, height, width));
2240
+ const scrollTop = Math.min(maxScroll, Math.max(0, state.scrollTop + step));
2241
+ return scrollTop === state.scrollTop ? state : { ...state, scrollTop };
2242
+ }
2243
+ function withScroll(state, model, height, width) {
2244
+ const scrollTop = followScrollTop(geometryFor(state, model, height, width), state.scrollTop);
2245
+ return { ...state, model, scrollTop };
2246
+ }
2247
+ function commitDraft(state) {
2248
+ const cleared = { ...state, draft: "", selection: null, mode: "browse" };
2249
+ if (state.selection === null) {
2250
+ return cleared;
2251
+ }
2252
+ const note = noteFromSelection(state.model, state.selection, state.nextNoteId, state.draft);
2253
+ if (note === null) {
2254
+ return cleared;
2255
+ }
2256
+ return {
2257
+ ...cleared,
2258
+ notes: sortNotes([...state.notes, note]),
2259
+ nextNoteId: state.nextNoteId + 1
2260
+ };
2261
+ }
2262
+ function enterNoteMode(state) {
2263
+ if (state.selection !== null) {
2264
+ return { ...state, mode: "note", draft: "" };
2265
+ }
2266
+ const rowIndex = cursorRowIndex(state.model);
2267
+ if (rowIndex === null) {
2268
+ return state;
2269
+ }
2270
+ return {
2271
+ ...state,
2272
+ selection: wholeRowSelection(state.model, rowIndex, NOTE_PANE),
2273
+ mode: "note",
2274
+ draft: ""
2275
+ };
2276
+ }
2277
+ function focusNextNote(state) {
2278
+ if (state.notes.length === 0) {
2279
+ return state;
2280
+ }
2281
+ const ids = state.notes.map((note2) => note2.id);
2282
+ const currentIndex = state.focusedNote === null ? -1 : ids.indexOf(state.focusedNote);
2283
+ const nextId = ids[(currentIndex + 1) % ids.length];
2284
+ const note = state.notes.find((candidate) => candidate.id === nextId);
2285
+ const model = { ...state.model, cursor: visibleIndexForRow2(state.model, note.rowIndex) };
2286
+ return { ...state, focusedNote: nextId, model };
2287
+ }
2288
+ function deleteNote(state, id) {
2289
+ const notes = state.notes.filter((note) => note.id !== id);
2290
+ if (state.focusedNote !== id) {
2291
+ return { ...state, notes };
2292
+ }
2293
+ if (notes.length === 0) {
2294
+ return { ...state, notes, focusedNote: null };
2295
+ }
2296
+ const deletedIndex = state.notes.findIndex((note) => note.id === id);
2297
+ const nextIndex = Math.min(deletedIndex, notes.length - 1);
2298
+ return { ...state, notes, focusedNote: notes[nextIndex].id };
2299
+ }
2300
+ function deleteFocusedNote(state) {
2301
+ return state.focusedNote === null ? state : deleteNote(state, state.focusedNote);
2302
+ }
2303
+ function applyConfirmKey(state, key) {
2304
+ if (key.ctrl) {
2305
+ return state;
2306
+ }
2307
+ if (key.name === "s") {
2308
+ return { ...state, quit: "send" };
2309
+ }
2310
+ if (key.name === "d") {
2311
+ return { ...state, quit: "clean" };
2312
+ }
2313
+ if (key.name === "escape") {
2314
+ return { ...state, mode: "browse" };
2315
+ }
2316
+ return state;
2317
+ }
2318
+ function applyNoteKey(state, key) {
2319
+ if (key.ctrl) {
2320
+ if (key.name === "s") {
2321
+ return { ...state, quit: "send" };
2322
+ }
2323
+ if (key.name === "q" || key.name === "c") {
2324
+ return state.notes.length === 0 ? { ...state, quit: "clean" } : { ...state, mode: "confirm" };
2325
+ }
2326
+ return state;
2327
+ }
2328
+ if (key.name === "enter") {
2329
+ return commitDraft(state);
2330
+ }
2331
+ if (key.name === "escape") {
2332
+ return { ...state, draft: "", mode: "browse" };
2333
+ }
2334
+ if (key.name === "backspace") {
2335
+ return { ...state, draft: state.draft.slice(0, -1) };
2336
+ }
2337
+ if (key.text !== "") {
2338
+ return { ...state, draft: state.draft + key.text };
2339
+ }
2340
+ return state;
2341
+ }
2342
+ function applyKey(state, key, height, width) {
2343
+ if (state.mode === "confirm") {
2344
+ return applyConfirmKey(state, key);
2345
+ }
2346
+ if (state.mode === "help") {
2347
+ if (!key.ctrl && key.name === "?") {
2348
+ return { ...state, mode: "browse" };
2349
+ }
2350
+ if (key.ctrl && key.name === "s") {
2351
+ return { ...state, quit: "send" };
2352
+ }
2353
+ if (key.ctrl && (key.name === "q" || key.name === "c")) {
2354
+ return state.notes.length === 0 ? { ...state, quit: "clean" } : { ...state, mode: "confirm" };
2355
+ }
2356
+ return state;
2357
+ }
2358
+ if (state.mode === "note") {
2359
+ return applyNoteKey(state, key);
2360
+ }
2361
+ if (key.ctrl) {
2362
+ if (key.name === "d") {
2363
+ return withScroll(
2364
+ state,
2365
+ moveCursor(state.model, pageSize(state, height, width)),
2366
+ height,
2367
+ width
2368
+ );
2369
+ }
2370
+ if (key.name === "u") {
2371
+ return withScroll(
2372
+ state,
2373
+ moveCursor(state.model, -pageSize(state, height, width)),
2374
+ height,
2375
+ width
2376
+ );
2377
+ }
2378
+ if (key.name === "s") {
2379
+ return { ...state, quit: "send" };
2380
+ }
2381
+ if (key.name === "q" || key.name === "c") {
2382
+ return state.notes.length === 0 ? { ...state, quit: "clean" } : { ...state, mode: "confirm" };
2383
+ }
2384
+ return state;
2385
+ }
2386
+ if (key.name === "s") {
2387
+ return { ...state, quit: "send" };
2388
+ }
2389
+ if (key.name === "q") {
2390
+ return state.notes.length === 0 ? { ...state, quit: "clean" } : { ...state, mode: "confirm" };
2391
+ }
2392
+ if (key.name === "v") {
2393
+ return startSelection(state);
2394
+ }
2395
+ if (key.name === "escape") {
2396
+ return { ...state, selection: null, mode: "browse" };
2397
+ }
2398
+ if (state.mode === "select") {
2399
+ if (key.name === "j" || key.name === "down") {
2400
+ return moveSelectionHead(state, 1);
2401
+ }
2402
+ if (key.name === "k" || key.name === "up") {
2403
+ return moveSelectionHead(state, -1);
2404
+ }
2405
+ }
2406
+ if (key.name === "a") {
2407
+ return enterNoteMode(state);
2408
+ }
2409
+ if (key.name === "tab") {
2410
+ return focusNextNote(state);
2411
+ }
2412
+ if (key.name === "d") {
2413
+ return deleteFocusedNote(state);
2414
+ }
2415
+ if (key.name === "j" || key.name === "down") {
2416
+ return withScroll(state, moveCursor(state.model, 1), height, width);
2417
+ }
2418
+ if (key.name === "k" || key.name === "up") {
2419
+ return withScroll(state, moveCursor(state.model, -1), height, width);
2420
+ }
2421
+ if (key.name === "n") {
2422
+ return withScroll(state, jumpToRun(state.model, 1), height, width);
2423
+ }
2424
+ if (key.name === "N") {
2425
+ return withScroll(state, jumpToRun(state.model, -1), height, width);
2426
+ }
2427
+ if (key.name === " ") {
2428
+ return withScroll(state, toggleFoldAtCursor(state.model), height, width);
2429
+ }
2430
+ if (key.name === "u") {
2431
+ return { ...state, layout: state.layout === "split" ? "unified" : "split" };
2432
+ }
2433
+ if (key.name === "?") {
2434
+ return { ...state, mode: "help" };
2435
+ }
2436
+ return state;
2437
+ }
2438
+ function frameDiff(previous, next) {
2439
+ return next.map((line, index) => line === previous[index] ? "" : `\x1B[${index + 1};1H${line}`).join("");
2440
+ }
2441
+ var ENTER_ALT_SCREEN = "\x1B[?1049h";
2442
+ var LEAVE_ALT_SCREEN = "\x1B[?1049l";
2443
+ var HIDE_CURSOR = "\x1B[?25l";
2444
+ var SHOW_CURSOR = "\x1B[?25h";
2445
+ function runTui(options, io, abort) {
2446
+ return new Promise((resolve, reject) => {
2447
+ const model = buildModel(options.before, options.after, options.context, options.minFold);
2448
+ let state = {
2449
+ model,
2450
+ mode: "browse",
2451
+ scrollTop: 0,
2452
+ map: { rows: [], panes: [] },
2453
+ layout: options.layout,
2454
+ quit: "none",
2455
+ selection: null,
2456
+ notes: [],
2457
+ focusedNote: null,
2458
+ draft: "",
2459
+ nextNoteId: 1,
2460
+ notePosition: options.notePosition
2461
+ };
2462
+ let previousLines = [];
2463
+ let finished = false;
2464
+ const repaint = () => {
2465
+ const { width, height } = io.size();
2466
+ const result = paint({
2467
+ model: state.model,
2468
+ width,
2469
+ height,
2470
+ path: options.path,
2471
+ tokens: options.tokens,
2472
+ truecolor: options.truecolor,
2473
+ rowBand: options.rowBand,
2474
+ scrollTop: state.scrollTop,
2475
+ layout: state.layout,
2476
+ selection: state.selection,
2477
+ mode: state.mode,
2478
+ draft: state.draft,
2479
+ notes: state.notes,
2480
+ focusedNote: state.focusedNote,
2481
+ notePosition: state.notePosition
2482
+ });
2483
+ state = { ...state, map: result.map };
2484
+ io.write(frameDiff(previousLines, result.lines));
2485
+ previousLines = result.lines;
2486
+ };
2487
+ const attemptTeardown = () => {
2488
+ let teardownError = null;
2489
+ try {
2490
+ io.write(SHOW_CURSOR);
2491
+ io.write(MOUSE_OFF);
2492
+ io.write(LEAVE_ALT_SCREEN);
2493
+ } catch (writeError) {
2494
+ teardownError = writeError instanceof Error ? writeError : new Error(String(writeError));
2495
+ }
2496
+ try {
2497
+ io.cleanup();
2498
+ } catch (cleanupError) {
2499
+ teardownError = teardownError ?? (cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError)));
2500
+ }
2501
+ return teardownError;
2502
+ };
2503
+ const finishQuit = (quit) => {
2504
+ finished = true;
2505
+ if (quit === "send") {
2506
+ writeResult(options.resultFile, state.notes);
2507
+ }
2508
+ const teardownError = attemptTeardown();
2509
+ if (teardownError !== null) {
2510
+ reject(teardownError);
2511
+ return;
2512
+ }
2513
+ const questions = quit === "send" ? toQuestions(state.notes) : [];
2514
+ resolve({ quit, questions });
2515
+ };
2516
+ const finishError = (error) => {
2517
+ if (finished) {
2518
+ return;
2519
+ }
2520
+ finished = true;
2521
+ attemptTeardown();
2522
+ reject(error instanceof Error ? error : new Error(String(error)));
2523
+ };
2524
+ abort?.addEventListener(
2525
+ "abort",
2526
+ () => {
2527
+ if (!finished) {
2528
+ finishQuit("clean");
2529
+ }
2530
+ },
2531
+ { once: true }
2532
+ );
2533
+ try {
2534
+ io.write(ENTER_ALT_SCREEN);
2535
+ io.write(MOUSE_ON);
2536
+ io.write(HIDE_CURSOR);
2537
+ repaint();
2538
+ } catch (error) {
2539
+ finishError(error);
2540
+ return;
2541
+ }
2542
+ io.onResize?.(() => {
2543
+ if (finished) {
2544
+ return;
2545
+ }
2546
+ try {
2547
+ previousLines = [];
2548
+ repaint();
2549
+ } catch (error) {
2550
+ finishError(error);
2551
+ }
2552
+ });
2553
+ io.onKey((chunk) => {
2554
+ if (finished) {
2555
+ return;
2556
+ }
2557
+ try {
2558
+ const { keys, mouse } = splitInput(chunk);
2559
+ state = mouse.reduce(
2560
+ (current, event) => event.kind === "scroll" && !event.shift ? applyScroll(current, event, io.size().height, io.size().width) : applyMouse(current, event),
2561
+ state
2562
+ );
2563
+ const events = parseKeys(keys);
2564
+ state = events.reduce(
2565
+ (current, event) => applyKey(current, event, io.size().height, io.size().width),
2566
+ state
2567
+ );
2568
+ repaint();
2569
+ if (state.quit !== "none") {
2570
+ finishQuit(state.quit);
2571
+ }
2572
+ } catch (error) {
2573
+ finishError(error);
2574
+ }
2575
+ });
2576
+ });
2577
+ }
2578
+
2579
+ // src/tui/index.ts
2580
+ var DEFAULT_WIDTH = 80;
2581
+ var DEFAULT_HEIGHT = 24;
2582
+ function createStdioIo() {
2583
+ const wasRaw = process.stdin.isRaw === true;
2584
+ process.stdin.setEncoding("utf8");
2585
+ if (process.stdin.isTTY) {
2586
+ process.stdin.setRawMode(true);
2587
+ }
2588
+ process.stdin.resume();
2589
+ return {
2590
+ onKey(handler) {
2591
+ process.stdin.on("data", (chunk) => {
2592
+ handler(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
2593
+ });
2594
+ },
2595
+ onResize(handler) {
2596
+ process.stdout.on("resize", handler);
2597
+ },
2598
+ write(text) {
2599
+ process.stdout.write(text);
2600
+ },
2601
+ size() {
2602
+ return {
2603
+ width: process.stdout.columns ?? DEFAULT_WIDTH,
2604
+ height: process.stdout.rows ?? DEFAULT_HEIGHT
2605
+ };
2606
+ },
2607
+ cleanup() {
2608
+ if (process.stdin.isTTY) {
2609
+ process.stdin.setRawMode(wasRaw);
2610
+ }
2611
+ process.stdin.pause();
2612
+ }
2613
+ };
2614
+ }
2615
+
2616
+ // src/tui/cli.ts
2617
+ var DEFAULT_TERMINAL_WIDTH = 80;
2618
+ var DEFAULT_TERMINAL_HEIGHT = 24;
2619
+ var DEFAULT_LAYOUT = "split";
2620
+ var DEFAULT_NOTE_POSITION = "panel";
2621
+ var UNIFIED_LAYOUT_FLAG = "unified";
2622
+ var ANCHORED_NOTES_FLAG = "anchored";
2623
+ var TRUE_FLAG_VALUE = "true";
2624
+ var FALSE_FLAG_VALUE = "false";
2625
+ function readLinesOf(path) {
2626
+ if (path === void 0) {
2627
+ return [];
2628
+ }
2629
+ try {
2630
+ return splitLines(readFileSync(path, "utf-8"));
2631
+ } catch {
2632
+ return [];
2633
+ }
2634
+ }
2635
+ function parseArgs(argv) {
2636
+ const args = {};
2637
+ argv.forEach((entry, index) => {
2638
+ if (!entry.startsWith("--")) {
2639
+ return;
2640
+ }
2641
+ const value = argv[index + 1];
2642
+ if (value === void 0 || value.startsWith("--")) {
2643
+ return;
2644
+ }
2645
+ args[entry.slice(2)] = value;
2646
+ });
2647
+ return args;
2648
+ }
2649
+ function toLayout(value) {
2650
+ return value === UNIFIED_LAYOUT_FLAG ? "unified" : DEFAULT_LAYOUT;
2651
+ }
2652
+ function toNotePosition(value) {
2653
+ return value === ANCHORED_NOTES_FLAG ? "anchored" : DEFAULT_NOTE_POSITION;
2654
+ }
2655
+ function toBoolean(value, fallback) {
2656
+ if (value === TRUE_FLAG_VALUE) {
2657
+ return true;
2658
+ }
2659
+ if (value === FALSE_FLAG_VALUE) {
2660
+ return false;
2661
+ }
2662
+ return fallback;
2663
+ }
2664
+ function toPositiveInteger(value, fallback) {
2665
+ if (value === void 0) {
2666
+ return fallback;
2667
+ }
2668
+ const parsed = Number(value);
2669
+ if (!Number.isInteger(parsed) || parsed <= 0) {
2670
+ return fallback;
2671
+ }
2672
+ return parsed;
2673
+ }
2674
+ function readTerminalSize(stdout) {
2675
+ return {
2676
+ width: stdout.columns ?? DEFAULT_TERMINAL_WIDTH,
2677
+ height: stdout.rows ?? DEFAULT_TERMINAL_HEIGHT
2678
+ };
2679
+ }
2680
+ async function run() {
2681
+ const args = parseArgs(process.argv.slice(2));
2682
+ const before = readLinesOf(args["left"]);
2683
+ const after = readLinesOf(args["right"]);
2684
+ const path = args["path"] ?? "";
2685
+ const layout = toLayout(args["layout"]);
2686
+ const notePosition = toNotePosition(args["notes"]);
2687
+ const rowBand = toBoolean(args["row-band"], true);
2688
+ const syntaxEnabled = toBoolean(args["syntax"], true);
2689
+ const context = toPositiveInteger(args["context"], DEFAULT_CONTEXT);
2690
+ const minFold = toPositiveInteger(args["min-fold"], DEFAULT_MIN_FOLD);
2691
+ const resultFile = args["result"] ?? resultFilePath();
2692
+ const truecolor = supportsTruecolor(process.env);
2693
+ const { width, height } = readTerminalSize(process.stdout);
2694
+ const tokens = await createTokenProvider({ path, enabled: syntaxEnabled, truecolor });
2695
+ const options = {
2696
+ before,
2697
+ after,
2698
+ path,
2699
+ context,
2700
+ minFold,
2701
+ layout,
2702
+ notePosition,
2703
+ rowBand,
2704
+ width,
2705
+ height,
2706
+ truecolor,
2707
+ resultFile,
2708
+ tokens
2709
+ };
2710
+ await runTui(options, createStdioIo());
2711
+ return 0;
2712
+ }
2713
+ async function main() {
2714
+ try {
2715
+ return await run();
2716
+ } catch (error) {
2717
+ const message = error instanceof Error ? error.message : String(error);
2718
+ process.stderr.write(`pair-mode tui: ${message}
2719
+ `);
2720
+ return 1;
2721
+ }
2722
+ }
2723
+ if (isEntryPoint(import.meta.url)) {
2724
+ const code = await main();
2725
+ process.exit(code);
2726
+ }
2727
+ export {
2728
+ toPositiveInteger
2729
+ };