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/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, false);
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 highlightJsFamily = (source, ts = false) => {
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, highlightJsFamily);
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, false);
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
- let statusW = 0;
580
- let countW = 0;
601
+ const widths = {};
602
+ for (const key of FILE_COL_KEYS) widths[key] = 0;
581
603
  for (const entry of files) {
582
- const status = entry.status ?? '';
583
- const count = `${entry.remaining ?? 0}`;
584
- const sw = visibleWidth(status);
585
- const cw = visibleWidth(count);
586
- if (sw > statusW) statusW = sw;
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 { statusW, countW };
610
+ return widths;
590
611
  };
591
612
 
592
- const padStartVisible = (text, width) => {
613
+ const padVisible = (text, width, align) => {
593
614
  const n = Math.max(0, width - visibleWidth(text));
594
- return `${' '.repeat(n)}${text}`;
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 status = entry.status ?? '';
609
- const count = `${entry.remaining ?? 0}`;
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 clipped = truncateVisible(entry.path, pathW);
664
+ const { path, status } = entry;
665
+ const clipped = truncateVisible(path, pathW);
617
666
  const pad = Math.max(0, pathW - visibleWidth(clipped));
618
- const line = `${prefix}${clipped}${' '.repeat(pad)}${suffix}`;
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 row = paintBar(line, inner, fgRgb, bgRgb, color);
622
- return row + fill(1, fgRgb, bgRgb, color);
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) => {