reslop 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -1
- package/README.md +18 -17
- package/lib/cli.js +22 -8
- package/lib/detect.js +2 -0
- package/lib/files.js +36 -12
- package/lib/git.js +312 -56
- package/lib/highlight.js +160 -5
- package/lib/keys.js +8 -2
- package/lib/render.js +78 -24
- package/lib/session.js +207 -22
- package/package.json +1 -1
package/lib/highlight.js
CHANGED
|
@@ -546,7 +546,7 @@ const highlightTemplate = (source, i, highlightExpr) => {
|
|
|
546
546
|
emit(parts, 'interpolation', '${');
|
|
547
547
|
j += 2;
|
|
548
548
|
const interp = readTemplateInterp(source, j);
|
|
549
|
-
const inner = highlightExpr(interp.expr
|
|
549
|
+
const inner = highlightExpr(interp.expr);
|
|
550
550
|
for (const t of inner) parts.push(t);
|
|
551
551
|
j = interp.end;
|
|
552
552
|
if (source[j] === '}') {
|
|
@@ -562,7 +562,149 @@ const highlightTemplate = (source, i, highlightExpr) => {
|
|
|
562
562
|
return { tokens: parts, end: j };
|
|
563
563
|
};
|
|
564
564
|
|
|
565
|
-
const
|
|
565
|
+
const isJsxTagStart = (source, i) => {
|
|
566
|
+
if (source[i] !== '<') return false;
|
|
567
|
+
if (i > 0 && /[A-Za-z0-9_$]/.test(source[i - 1])) return false;
|
|
568
|
+
const next = source[i + 1];
|
|
569
|
+
if (next === '>' || next === '/') return true;
|
|
570
|
+
if (!/[A-Za-z_$]/.test(next || '')) return false;
|
|
571
|
+
let j = i + 1;
|
|
572
|
+
while (j < source.length && /[A-Za-z0-9_$.:-]/.test(source[j])) j += 1;
|
|
573
|
+
const after = source[skipWs(source, j)];
|
|
574
|
+
if (after === '>' || after === '/' || after === '{') return true;
|
|
575
|
+
return /[A-Za-z_$]/.test(after || '');
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const jsxNameStyle = (name) => {
|
|
579
|
+
if (/^[A-Z]/.test(name) || name.includes('.')) return 'className';
|
|
580
|
+
return 'tag';
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const readJsxName = (source, i) => {
|
|
584
|
+
let j = i;
|
|
585
|
+
while (j < source.length && /[A-Za-z0-9_$.:-]/.test(source[j])) j += 1;
|
|
586
|
+
return { text: source.slice(i, j), end: j };
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const highlightJsxExpr = (source, i, inner) => {
|
|
590
|
+
const out = [];
|
|
591
|
+
emit(out, 'interpolation', '{');
|
|
592
|
+
const interp = readTemplateInterp(source, i + 1);
|
|
593
|
+
const tokens = inner(interp.expr);
|
|
594
|
+
for (const t of tokens) out.push(t);
|
|
595
|
+
let end = interp.end;
|
|
596
|
+
if (source[end] === '}') {
|
|
597
|
+
emit(out, 'interpolation', '}');
|
|
598
|
+
end += 1;
|
|
599
|
+
}
|
|
600
|
+
return { tokens: out, end };
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const highlightJsxTagTail = (source, i, inner, out) => {
|
|
604
|
+
const n = source.length;
|
|
605
|
+
let selfClosing = false;
|
|
606
|
+
while (i < n) {
|
|
607
|
+
const ch = source[i];
|
|
608
|
+
if (ch === '>') {
|
|
609
|
+
emit(out, 'punct', '>');
|
|
610
|
+
i += 1;
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
if (ch === '/' && source[i + 1] === '>') {
|
|
614
|
+
emit(out, 'punct', '/>');
|
|
615
|
+
i += 2;
|
|
616
|
+
selfClosing = true;
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
if (/\s/.test(ch)) {
|
|
620
|
+
emit(out, 'plain', ch);
|
|
621
|
+
i += 1;
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (ch === '{') {
|
|
625
|
+
const expr = highlightJsxExpr(source, i, inner);
|
|
626
|
+
for (const t of expr.tokens) out.push(t);
|
|
627
|
+
i = expr.end;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (QUOTES.includes(ch)) {
|
|
631
|
+
const { text, end } = readString(source, i);
|
|
632
|
+
emit(out, 'string', text);
|
|
633
|
+
i = end;
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (ch === '=') {
|
|
637
|
+
emit(out, 'punct', '=');
|
|
638
|
+
i += 1;
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
if (/[A-Za-z_$]/.test(ch)) {
|
|
642
|
+
const { text, end } = readJsxName(source, i);
|
|
643
|
+
emit(out, 'attr', text);
|
|
644
|
+
i = end;
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
emit(out, 'punct', ch);
|
|
648
|
+
i += 1;
|
|
649
|
+
}
|
|
650
|
+
return { end: i, selfClosing };
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const highlightJsxElement = (source, i, inner) => {
|
|
654
|
+
const out = [];
|
|
655
|
+
const n = source.length;
|
|
656
|
+
emit(out, 'punct', '<');
|
|
657
|
+
i += 1;
|
|
658
|
+
let closing = false;
|
|
659
|
+
if (source[i] === '/') {
|
|
660
|
+
emit(out, 'punct', '/');
|
|
661
|
+
i += 1;
|
|
662
|
+
closing = true;
|
|
663
|
+
}
|
|
664
|
+
const nameRead = readJsxName(source, i);
|
|
665
|
+
const name = nameRead.text;
|
|
666
|
+
if (name) {
|
|
667
|
+
emit(out, jsxNameStyle(name), name);
|
|
668
|
+
i = nameRead.end;
|
|
669
|
+
}
|
|
670
|
+
const tail = highlightJsxTagTail(source, i, inner, out);
|
|
671
|
+
i = tail.end;
|
|
672
|
+
if (closing || tail.selfClosing) return { tokens: out, end: i };
|
|
673
|
+
while (i < n) {
|
|
674
|
+
if (source.startsWith('</', i)) {
|
|
675
|
+
const close = highlightJsxElement(source, i, inner);
|
|
676
|
+
for (const t of close.tokens) out.push(t);
|
|
677
|
+
return { tokens: out, end: close.end };
|
|
678
|
+
}
|
|
679
|
+
if (source[i] === '{') {
|
|
680
|
+
const expr = highlightJsxExpr(source, i, inner);
|
|
681
|
+
for (const t of expr.tokens) out.push(t);
|
|
682
|
+
i = expr.end;
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (source[i] === '<' && isJsxTagStart(source, i)) {
|
|
686
|
+
const child = highlightJsxElement(source, i, inner);
|
|
687
|
+
for (const t of child.tokens) out.push(t);
|
|
688
|
+
i = child.end;
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
let j = i;
|
|
692
|
+
while (j < n && source[j] !== '<' && source[j] !== '{') j += 1;
|
|
693
|
+
if (j === i) {
|
|
694
|
+
emit(out, 'plain', source[i]);
|
|
695
|
+
i += 1;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
emit(out, 'plain', source.slice(i, j));
|
|
699
|
+
i = j;
|
|
700
|
+
}
|
|
701
|
+
return { tokens: out, end: i };
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
const highlightJsFamily = (source, options = {}) => {
|
|
705
|
+
const ts = options.ts === true;
|
|
706
|
+
const jsx = options.jsx === true;
|
|
707
|
+
const inner = (expr) => highlightJsFamily(expr, options);
|
|
566
708
|
const out = [];
|
|
567
709
|
let i = 0;
|
|
568
710
|
const n = source.length;
|
|
@@ -587,7 +729,7 @@ const highlightJsFamily = (source, ts = false) => {
|
|
|
587
729
|
}
|
|
588
730
|
|
|
589
731
|
if (ch === '`') {
|
|
590
|
-
const r = highlightTemplate(source, i,
|
|
732
|
+
const r = highlightTemplate(source, i, inner);
|
|
591
733
|
for (const t of r.tokens) out.push(t);
|
|
592
734
|
i = r.end;
|
|
593
735
|
continue;
|
|
@@ -636,6 +778,14 @@ const highlightJsFamily = (source, ts = false) => {
|
|
|
636
778
|
continue;
|
|
637
779
|
}
|
|
638
780
|
|
|
781
|
+
if (jsx && ch === '<' && isJsxTagStart(source, i)) {
|
|
782
|
+
const node = highlightJsxElement(source, i, inner);
|
|
783
|
+
for (const t of node.tokens) out.push(t);
|
|
784
|
+
i = node.end;
|
|
785
|
+
expectName = null;
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
|
|
639
789
|
if (/[=<>!+\-*/%&|^~?:]/.test(ch)) {
|
|
640
790
|
const { text, end } = readJsOperator(source, i);
|
|
641
791
|
emit(out, 'operator', text);
|
|
@@ -1136,13 +1286,18 @@ const highlightTxt = (source) => {
|
|
|
1136
1286
|
return out;
|
|
1137
1287
|
};
|
|
1138
1288
|
|
|
1139
|
-
const highlightJs = (source) => highlightJsFamily(source,
|
|
1140
|
-
const highlightTs = (source) => highlightJsFamily(source, true);
|
|
1289
|
+
const highlightJs = (source) => highlightJsFamily(source, {});
|
|
1290
|
+
const highlightTs = (source) => highlightJsFamily(source, { ts: true });
|
|
1291
|
+
const highlightJsx = (source) => highlightJsFamily(source, { jsx: true });
|
|
1292
|
+
const highlightTsx = (source) =>
|
|
1293
|
+
highlightJsFamily(source, { ts: true, jsx: true });
|
|
1141
1294
|
|
|
1142
1295
|
const HIGHLIGHTERS = {
|
|
1143
1296
|
js: highlightJs,
|
|
1144
1297
|
mjs: highlightJs,
|
|
1145
1298
|
ts: highlightTs,
|
|
1299
|
+
jsx: highlightJsx,
|
|
1300
|
+
tsx: highlightTsx,
|
|
1146
1301
|
json: highlightJson,
|
|
1147
1302
|
css: highlightCss,
|
|
1148
1303
|
html: highlightHtml,
|
package/lib/keys.js
CHANGED
|
@@ -4,8 +4,8 @@ const ACTIONS = {
|
|
|
4
4
|
add: { keys: ['a'], label: 'Add' },
|
|
5
5
|
unstage: { keys: ['u'], label: 'Unstage' },
|
|
6
6
|
revert: { keys: ['r'], label: 'Revert' },
|
|
7
|
-
prev: { keys: ['left', 'p'], hot: '←', label: 'Prev' },
|
|
8
|
-
next: { keys: ['right', 'n'], hot: '→', label: 'Next' },
|
|
7
|
+
prev: { keys: ['left', 'p', 'k'], hot: '←', label: 'Prev' },
|
|
8
|
+
next: { keys: ['right', 'n', 'j'], hot: '→', label: 'Next' },
|
|
9
9
|
layout: { keys: ['m'], label: 'Mode' },
|
|
10
10
|
files: { keys: ['l'], label: 'Files' },
|
|
11
11
|
feedback: { keys: ['f'], label: 'Feedback' },
|
|
@@ -19,6 +19,12 @@ const FILES_DISABLED = ['layout', 'feedback'];
|
|
|
19
19
|
const SCROLL = {
|
|
20
20
|
up: 'scrollUp',
|
|
21
21
|
down: 'scrollDown',
|
|
22
|
+
'ctrl-y': 'scrollUp',
|
|
23
|
+
'ctrl-e': 'scrollDown',
|
|
24
|
+
'ctrl-b': 'pageUp',
|
|
25
|
+
'ctrl-f': 'pageDown',
|
|
26
|
+
'ctrl-u': 'halfUp',
|
|
27
|
+
'ctrl-d': 'halfDown',
|
|
22
28
|
enter: 'open',
|
|
23
29
|
backspace: 'removeTodo',
|
|
24
30
|
delete: 'removeTodo',
|
package/lib/render.js
CHANGED
|
@@ -103,6 +103,16 @@ const FILE_STATUS_FG = {
|
|
|
103
103
|
partial: THEME.warnFg,
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
+
const FILE_COL_KEYS = ['add', 'del', 'status', 'n', 'm'];
|
|
107
|
+
|
|
108
|
+
const FILE_COL_ALIGN = {
|
|
109
|
+
add: 'start',
|
|
110
|
+
del: 'start',
|
|
111
|
+
status: 'start',
|
|
112
|
+
n: 'start',
|
|
113
|
+
m: 'end',
|
|
114
|
+
};
|
|
115
|
+
|
|
106
116
|
const INFO_STATUS = [
|
|
107
117
|
'copied',
|
|
108
118
|
'staged',
|
|
@@ -575,23 +585,36 @@ const paintStatusLine = (view, status, width, color) => {
|
|
|
575
585
|
);
|
|
576
586
|
};
|
|
577
587
|
|
|
588
|
+
const signedCount = (sign, n) => `${sign}${n ?? 0}`;
|
|
589
|
+
|
|
590
|
+
const fileRowFields = (entry) => {
|
|
591
|
+
const { added, removed, staged, remaining } = entry;
|
|
592
|
+
const add = signedCount('+', added);
|
|
593
|
+
const del = signedCount('-', removed);
|
|
594
|
+
const status = entry.status ?? '';
|
|
595
|
+
const n = `${staged ?? 0}`;
|
|
596
|
+
const m = `${remaining ?? 0}`;
|
|
597
|
+
return { add, del, status, n, m };
|
|
598
|
+
};
|
|
599
|
+
|
|
578
600
|
const fileColumns = (files) => {
|
|
579
|
-
|
|
580
|
-
|
|
601
|
+
const widths = {};
|
|
602
|
+
for (const key of FILE_COL_KEYS) widths[key] = 0;
|
|
581
603
|
for (const entry of files) {
|
|
582
|
-
const
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
if (cw > countW) countW = cw;
|
|
604
|
+
const fields = fileRowFields(entry);
|
|
605
|
+
for (const key of FILE_COL_KEYS) {
|
|
606
|
+
const w = visibleWidth(fields[key]);
|
|
607
|
+
if (w > widths[key]) widths[key] = w;
|
|
608
|
+
}
|
|
588
609
|
}
|
|
589
|
-
return
|
|
610
|
+
return widths;
|
|
590
611
|
};
|
|
591
612
|
|
|
592
|
-
const
|
|
613
|
+
const padVisible = (text, width, align) => {
|
|
593
614
|
const n = Math.max(0, width - visibleWidth(text));
|
|
594
|
-
|
|
615
|
+
const gap = ' '.repeat(n);
|
|
616
|
+
if (align === 'end') return `${text}${gap}`;
|
|
617
|
+
return `${gap}${text}`;
|
|
595
618
|
};
|
|
596
619
|
|
|
597
620
|
const fileRowFg = (status, selected) => {
|
|
@@ -601,25 +624,55 @@ const fileRowFg = (status, selected) => {
|
|
|
601
624
|
return THEME.mutedFg;
|
|
602
625
|
};
|
|
603
626
|
|
|
627
|
+
const fileRowParts = (entry, cols) => {
|
|
628
|
+
const fields = fileRowFields(entry);
|
|
629
|
+
const parts = {};
|
|
630
|
+
for (const key of FILE_COL_KEYS) {
|
|
631
|
+
const width = cols?.[key] ?? visibleWidth(fields[key]);
|
|
632
|
+
const align = FILE_COL_ALIGN[key];
|
|
633
|
+
parts[key] = padVisible(fields[key], width, align);
|
|
634
|
+
}
|
|
635
|
+
return parts;
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
const joinFileSuffix = (parts) => {
|
|
639
|
+
const { add, del, status, n, m } = parts;
|
|
640
|
+
return ` ${add} ${del} ${status} ${n}/${m}`;
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const paintFileSuffix = (parts, fgRgb, bgRgb, color) => {
|
|
644
|
+
const { add, del, status, n, m } = parts;
|
|
645
|
+
const gap = paint(' ', fgRgb, bgRgb, color);
|
|
646
|
+
const addText = paint(add, THEME.addLineFg, bgRgb, color);
|
|
647
|
+
const delText = paint(del, THEME.delLineFg, bgRgb, color);
|
|
648
|
+
const statusText = paint(status, fgRgb, bgRgb, color);
|
|
649
|
+
const nText = paint(n, fgRgb, bgRgb, color);
|
|
650
|
+
const slash = paint('/', fgRgb, bgRgb, color);
|
|
651
|
+
const mText = paint(m, fgRgb, bgRgb, color);
|
|
652
|
+
const counts = addText + gap + delText;
|
|
653
|
+
const meta = statusText + gap + nText + slash + mText;
|
|
654
|
+
return gap + counts + gap + meta;
|
|
655
|
+
};
|
|
656
|
+
|
|
604
657
|
const paintFileRow = (entry, width, color, selected, cols) => {
|
|
605
658
|
const inner = Math.max(1, width - 1);
|
|
606
659
|
const mark = selected ? FILE_MARK : ' ';
|
|
607
660
|
const prefix = ` ${mark} `;
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
const statusW = cols?.statusW ?? visibleWidth(status);
|
|
611
|
-
const countW = cols?.countW ?? visibleWidth(count);
|
|
612
|
-
const statusCol = padStartVisible(status, statusW);
|
|
613
|
-
const countCol = padStartVisible(count, countW);
|
|
614
|
-
const suffix = ` ${statusCol} ${countCol}`;
|
|
661
|
+
const parts = fileRowParts(entry, cols);
|
|
662
|
+
const suffix = joinFileSuffix(parts);
|
|
615
663
|
const pathW = Math.max(1, inner - visibleWidth(prefix + suffix));
|
|
616
|
-
const
|
|
664
|
+
const { path, status } = entry;
|
|
665
|
+
const clipped = truncateVisible(path, pathW);
|
|
617
666
|
const pad = Math.max(0, pathW - visibleWidth(clipped));
|
|
618
|
-
const
|
|
667
|
+
const left = `${prefix}${clipped}${' '.repeat(pad)}`;
|
|
619
668
|
const fgRgb = fileRowFg(status, selected);
|
|
620
669
|
const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
|
|
621
|
-
const
|
|
622
|
-
|
|
670
|
+
const edge = fill(1, fgRgb, bgRgb, color);
|
|
671
|
+
if (!color) return `${left}${suffix}${edge}`;
|
|
672
|
+
let out = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
673
|
+
out += paint(left, fgRgb, bgRgb, color);
|
|
674
|
+
out += paintFileSuffix(parts, fgRgb, bgRgb, color);
|
|
675
|
+
return out + edge;
|
|
623
676
|
};
|
|
624
677
|
|
|
625
678
|
const todoInnerWidth = (width) => Math.max(1, width - 2);
|
|
@@ -950,7 +1003,7 @@ const stackFrame = (view, width, color, rows, layout, windowed, notes) => {
|
|
|
950
1003
|
return overlayRows(rows, view.selection);
|
|
951
1004
|
};
|
|
952
1005
|
|
|
953
|
-
const frameResult = (text, painted, layout, windowed, hits) => {
|
|
1006
|
+
const frameResult = (text, painted, layout, windowed, hits, bodyH) => {
|
|
954
1007
|
const fileHits = windowed.fileHits;
|
|
955
1008
|
const todoHits = windowed.todoHits;
|
|
956
1009
|
const templateHits = hits.templateHits;
|
|
@@ -968,6 +1021,7 @@ const frameResult = (text, painted, layout, windowed, hits) => {
|
|
|
968
1021
|
height: painted.length,
|
|
969
1022
|
scroll,
|
|
970
1023
|
cursor,
|
|
1024
|
+
bodyH,
|
|
971
1025
|
};
|
|
972
1026
|
};
|
|
973
1027
|
|
|
@@ -999,7 +1053,7 @@ const renderFrame = (view, options = {}) => {
|
|
|
999
1053
|
notes.noteCursor,
|
|
1000
1054
|
bodyH,
|
|
1001
1055
|
);
|
|
1002
|
-
return frameResult(text, painted, layout, windowed, hits);
|
|
1056
|
+
return frameResult(text, painted, layout, windowed, hits, bodyH);
|
|
1003
1057
|
};
|
|
1004
1058
|
|
|
1005
1059
|
const presentCursor = (cursor) => {
|