gitmaps 1.1.18 → 1.1.20

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/app/analytics.db CHANGED
Binary file
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import { collectPreviewDiffMarkers, estimatePreviewCharsPerLine, estimatePreviewLineCapacity, estimatePreviewMaxScroll, estimateTitleCharsPerLine, getLowZoomPreviewText, getLowZoomScale, getPreviewScrollMetrics, wrapPreviewText } from './low-zoom-preview';
2
+ import { collectPreviewDiffMarkers, estimatePreviewCharsPerLine, estimatePreviewLineCapacity, estimatePreviewMaxScroll, estimateTitleCharsPerLine, getLowZoomPreviewText, getLowZoomScale, getPreviewRenderableLines, getPreviewScrollMetrics, wrapPreviewText } from './low-zoom-preview';
3
3
 
4
4
  describe('low zoom preview helpers', () => {
5
5
  test('anchors preview text to approximate saved scroll position', () => {
@@ -21,6 +21,18 @@ describe('low zoom preview helpers', () => {
21
21
  expect(getLowZoomPreviewText({ path: 'bin.dat', ext: 'dat', isBinary: true, content: 'abc' }, 0)).toBe('');
22
22
  });
23
23
 
24
+ test('preview renderable lines preserve added and deleted diff context', () => {
25
+ const lines = getPreviewRenderableLines({
26
+ path: 'src/example.ts',
27
+ ext: 'ts',
28
+ content: ['one', 'two', 'three'].join('\n'),
29
+ addedLines: new Set([2]),
30
+ deletedBeforeLine: new Map([[2, ['old-two']]]),
31
+ }, 0);
32
+ expect(lines.some((line) => line.tone === 'deleted' && line.text.includes('old-two'))).toBe(true);
33
+ expect(lines.some((line) => line.tone === 'added' && line.text === 'two')).toBe(true);
34
+ });
35
+
24
36
  test('keeps zoomed-out on-screen text smaller than zoomed-in text', () => {
25
37
  const far = getLowZoomScale(0.1);
26
38
  const near = getLowZoomScale(1);
@@ -76,6 +88,25 @@ describe('low zoom preview helpers', () => {
76
88
  expect(deleted[0]?.height).toBe(1);
77
89
  });
78
90
 
91
+ test('derives diff markers from hunks when line maps are absent', () => {
92
+ const markers = collectPreviewDiffMarkers({
93
+ hunks: [
94
+ {
95
+ newStart: 8,
96
+ lines: [
97
+ { type: 'ctx', content: 'a' },
98
+ { type: 'del', content: 'old-1' },
99
+ { type: 'del', content: 'old-2' },
100
+ { type: 'add', content: 'new-1' },
101
+ { type: 'ctx', content: 'b' },
102
+ ],
103
+ },
104
+ ],
105
+ }, 20);
106
+ expect(markers.some((m) => m.color === '#22c55e')).toBe(true);
107
+ expect(markers.some((m) => m.color === '#ef4444')).toBe(true);
108
+ });
109
+
79
110
  test('title typography is larger than body typography for readability', () => {
80
111
  const scale = getLowZoomScale(0.18);
81
112
  expect(scale.titleFont).toBeGreaterThan(scale.bodyFont);
@@ -25,17 +25,81 @@ export function getLowZoomScale(zoom: number) {
25
25
  };
26
26
  }
27
27
 
28
- export function getLowZoomPreviewText(file: any, scrollTop: number): string {
29
- if (!file || file.isBinary || !file.content) return '';
28
+ type PreviewRenderableLine = {
29
+ text: string;
30
+ tone: 'normal' | 'added' | 'deleted';
31
+ sourceLine?: number;
32
+ };
33
+
34
+ function getPreviewDiffMaps(file: any) {
35
+ const addedLines = new Set<number>(file?.addedLines instanceof Set ? Array.from(file.addedLines) : []);
36
+ const deletedBeforeLine = new Map<number, string[]>(file?.deletedBeforeLine instanceof Map ? Array.from(file.deletedBeforeLine.entries()) : []);
37
+
38
+ if ((addedLines.size === 0 && deletedBeforeLine.size === 0) && Array.isArray(file?.hunks)) {
39
+ for (const hunk of file.hunks) {
40
+ let newLine = hunk?.newStart || 1;
41
+ let pendingDeleted: string[] = [];
42
+ for (const line of hunk?.lines || []) {
43
+ if (line?.type === 'add') {
44
+ addedLines.add(newLine);
45
+ if (pendingDeleted.length > 0) {
46
+ const existing = deletedBeforeLine.get(newLine) || [];
47
+ deletedBeforeLine.set(newLine, existing.concat(pendingDeleted));
48
+ pendingDeleted = [];
49
+ }
50
+ newLine += 1;
51
+ } else if (line?.type === 'del') {
52
+ pendingDeleted.push(String(line?.content || ''));
53
+ } else {
54
+ if (pendingDeleted.length > 0) {
55
+ const existing = deletedBeforeLine.get(newLine) || [];
56
+ deletedBeforeLine.set(newLine, existing.concat(pendingDeleted));
57
+ pendingDeleted = [];
58
+ }
59
+ newLine += 1;
60
+ }
61
+ }
62
+ if (pendingDeleted.length > 0) {
63
+ const existing = deletedBeforeLine.get(newLine) || [];
64
+ deletedBeforeLine.set(newLine, existing.concat(pendingDeleted));
65
+ }
66
+ }
67
+ }
68
+
69
+ return { addedLines, deletedBeforeLine };
70
+ }
71
+
72
+ export function getPreviewRenderableLines(file: any, scrollTop: number): PreviewRenderableLine[] {
73
+ if (!file || file.isBinary || !file.content) return [];
30
74
 
31
75
  const ext = (file.ext || file.path?.split('.').pop() || '').toLowerCase();
32
- if (!PREVIEWABLE_EXTS.has(ext)) return '';
76
+ if (!PREVIEWABLE_EXTS.has(ext)) return [];
33
77
 
34
78
  const normalized = String(file.content).replace(/\t/g, ' ');
35
79
  const lines = normalized.split('\n');
36
80
  const approxLineHeight = 20;
37
81
  const startLine = Math.max(0, Math.floor(scrollTop / approxLineHeight));
38
- return lines.slice(startLine).join('\n').trim();
82
+ const { addedLines, deletedBeforeLine } = getPreviewDiffMaps(file);
83
+ const out: PreviewRenderableLine[] = [];
84
+
85
+ for (let i = startLine; i < lines.length; i += 1) {
86
+ const lineNum = i + 1;
87
+ const deleted = deletedBeforeLine.get(lineNum) || [];
88
+ for (const deletedLine of deleted) {
89
+ out.push({ text: `- ${String(deletedLine).replace(/\t/g, ' ')}`, tone: 'deleted', sourceLine: lineNum });
90
+ }
91
+ out.push({
92
+ text: lines[i],
93
+ tone: file?.status === 'added' || addedLines.has(lineNum) ? 'added' : file?.status === 'deleted' ? 'deleted' : 'normal',
94
+ sourceLine: lineNum,
95
+ });
96
+ }
97
+
98
+ return out;
99
+ }
100
+
101
+ export function getLowZoomPreviewText(file: any, scrollTop: number): string {
102
+ return getPreviewRenderableLines(file, scrollTop).map((line) => line.text).join('\n').trim();
39
103
  }
40
104
 
41
105
  export function wrapPreviewText(text: string, maxCharsPerLine: number, maxLines: number): string[] {
@@ -137,8 +201,7 @@ export function getPreviewScrollMetrics(file: any, height: number, zoom: number,
137
201
  export function collectPreviewDiffMarkers(file: any, totalLines: number) {
138
202
  const markers: Array<{ ratio: number; color: string; height?: number }> = [];
139
203
  const safeTotal = Math.max(1, totalLines);
140
- const added = file?.addedLines instanceof Set ? Array.from(file.addedLines) : [];
141
- const deletedBefore = file?.deletedBeforeLine instanceof Map ? Array.from(file.deletedBeforeLine.keys()) : [];
204
+ const { addedLines, deletedBeforeLine } = getPreviewDiffMaps(file);
142
205
 
143
206
  if (file?.status === 'added') {
144
207
  markers.push({ ratio: 0, color: '#22c55e', height: 1 });
@@ -149,10 +212,10 @@ export function collectPreviewDiffMarkers(file: any, totalLines: number) {
149
212
  return markers;
150
213
  }
151
214
 
152
- for (const line of added) {
215
+ for (const line of addedLines) {
153
216
  markers.push({ ratio: Math.max(0, Math.min(1, (line - 1) / safeTotal)), color: '#22c55e' });
154
217
  }
155
- for (const line of deletedBefore) {
218
+ for (const line of deletedBeforeLine.keys()) {
156
219
  markers.push({ ratio: Math.max(0, Math.min(1, (line - 1) / safeTotal)), color: '#ef4444' });
157
220
  }
158
221
  return markers;
@@ -223,55 +286,52 @@ export function renderLowZoomPreviewCanvas(
223
286
  ctx.fillText(trimToWidth(ctx, subtitle, maxTextWidth), leftInset, subtitleY);
224
287
 
225
288
  const previewY = subtitleY + subtitleFont + scale.gap;
226
- const rawPreview = getLowZoomPreviewText(file, scrollTop) || 'Preview unavailable';
227
- const wrapped = wrapPreviewText(
228
- rawPreview,
229
- estimatePreviewCharsPerLine(width, zoom),
230
- estimatePreviewLineCapacity(height, zoom),
231
- );
289
+ const previewLines = getPreviewRenderableLines(file, scrollTop);
290
+ const maxCharsPerLine = estimatePreviewCharsPerLine(width, zoom);
291
+ const maxVisibleLines = estimatePreviewLineCapacity(height, zoom);
292
+ const wrapped: Array<{ text: string; tone: 'normal' | 'added' | 'deleted' }> = [];
293
+
294
+ for (const line of previewLines) {
295
+ const parts = wrapPreviewText(line.text, maxCharsPerLine, Math.max(1, maxVisibleLines - wrapped.length));
296
+ for (const part of parts) {
297
+ wrapped.push({ text: part, tone: line.tone });
298
+ if (wrapped.length >= maxVisibleLines) break;
299
+ }
300
+ if (wrapped.length >= maxVisibleLines) break;
301
+ }
302
+
303
+ if (wrapped.length === 0) {
304
+ wrapped.push({ text: 'Preview unavailable', tone: 'normal' });
305
+ }
232
306
 
233
307
  ctx.font = `${scale.bodyFont}px "JetBrains Mono", monospace`;
234
- ctx.fillStyle = 'rgba(226,232,240,0.92)';
235
308
 
236
309
  const fadeStart = Math.max(previewY, height - scale.bodyLineHeight * 2.2);
237
310
  const bodyHeight = Math.max(scale.bodyLineHeight * 2, height - previewY - scale.padding);
238
- const mask = ctx.createLinearGradient(0, previewY, 0, previewY + bodyHeight);
239
- mask.addColorStop(0, 'rgba(226,232,240,0.92)');
240
- mask.addColorStop(Math.max(0, (fadeStart - previewY) / Math.max(1, bodyHeight)), 'rgba(226,232,240,0.92)');
241
- mask.addColorStop(1, 'rgba(226,232,240,0)');
242
- ctx.fillStyle = mask;
311
+ const fadeRatio = Math.max(0, (fadeStart - previewY) / Math.max(1, bodyHeight));
243
312
 
244
313
  wrapped.forEach((line, index) => {
245
314
  const y = previewY + index * scale.bodyLineHeight;
246
315
  if (y > height - scale.padding) return;
247
- ctx.fillText(line, leftInset, y);
248
- });
249
-
250
- const trackX = width - scrollbarWidth - 5;
251
- const markerX = trackX - markerLaneWidth - 4;
252
- const trackY = scrollMetrics.trackPadding;
253
- const trackHeight = scrollMetrics.trackHeight;
254
316
 
255
- ctx.fillStyle = 'rgba(255,255,255,0.08)';
256
- roundRect(ctx, markerX, trackY, markerLaneWidth, trackHeight, 3);
257
- ctx.fill();
258
-
259
- ctx.fillStyle = 'rgba(255,255,255,0.12)';
260
- roundRect(ctx, trackX, trackY, scrollbarWidth, trackHeight, scrollbarWidth / 2);
261
- ctx.fill();
317
+ const t = Math.max(0, Math.min(1, (y - previewY) / Math.max(1, bodyHeight)));
318
+ const alpha = t <= fadeRatio ? 1 : Math.max(0, 1 - ((t - fadeRatio) / Math.max(0.0001, 1 - fadeRatio)));
319
+
320
+ if (line.tone === 'added') {
321
+ ctx.fillStyle = `rgba(34,197,94,${0.12 * alpha})`;
322
+ ctx.fillRect(leftInset - 4, y, maxTextWidth + 4, scale.bodyLineHeight - 1);
323
+ ctx.fillStyle = `rgba(134,239,172,${0.98 * alpha})`;
324
+ } else if (line.tone === 'deleted') {
325
+ ctx.fillStyle = `rgba(239,68,68,${0.1 * alpha})`;
326
+ ctx.fillRect(leftInset - 4, y, maxTextWidth + 4, scale.bodyLineHeight - 1);
327
+ ctx.fillStyle = `rgba(252,165,165,${0.96 * alpha})`;
328
+ } else {
329
+ ctx.fillStyle = `rgba(226,232,240,${0.92 * alpha})`;
330
+ }
262
331
 
263
- const markers = collectPreviewDiffMarkers(file, scrollMetrics.totalLines);
264
- for (const marker of markers) {
265
- const markerHeight = marker.height === 1 ? trackHeight : 5;
266
- const y = marker.height === 1 ? trackY : trackY + marker.ratio * Math.max(0, trackHeight - markerHeight);
267
- ctx.fillStyle = marker.color;
268
- roundRect(ctx, markerX, Math.max(trackY, y), markerLaneWidth, markerHeight, 2);
269
- ctx.fill();
270
- }
332
+ ctx.fillText(trimToWidth(ctx, line.text, maxTextWidth), leftInset, y);
333
+ });
271
334
 
272
- ctx.fillStyle = 'rgba(196,181,253,0.96)';
273
- roundRect(ctx, trackX, scrollMetrics.thumbY, scrollbarWidth, scrollMetrics.thumbHeight, scrollbarWidth / 2);
274
- ctx.fill();
275
335
  }
276
336
 
277
337
  function trimToWidth(ctx: CanvasRenderingContext2D, text: string, maxWidth: number) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitmaps",
3
- "version": "1.1.18",
3
+ "version": "1.1.20",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "gitmaps": "cli.ts"