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 +0 -0
- package/app/lib/low-zoom-preview.test.ts +32 -1
- package/app/lib/low-zoom-preview.ts +105 -45
- package/package.json +1 -1
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
264
|
-
|
|
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) {
|