gitmaps 1.1.0 → 1.1.2
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/README.md +267 -118
- package/app/[...slug]/page.client.tsx +1 -0
- package/app/[...slug]/page.tsx +6 -0
- package/app/analytics.db +0 -0
- package/app/api/analytics/route.ts +64 -0
- package/app/api/auth/positions/route.ts +95 -33
- package/app/api/build-info/route.ts +19 -0
- package/app/api/chat/route.ts +13 -2
- package/app/api/og-image/route.ts +14 -0
- package/app/api/repo/file-content/route.ts +73 -20
- package/app/api/repo/load/route.test.ts +62 -0
- package/app/api/repo/load/route.ts +41 -1
- package/app/api/repo/pdf-thumb/route.ts +127 -0
- package/app/api/repo/resolve-slug/route.ts +51 -0
- package/app/api/repo/tree/route.ts +188 -104
- package/app/api/version/route.ts +26 -0
- package/app/globals.css +5706 -4938
- package/app/layout.tsx +1279 -490
- package/app/lib/auto-arrange.test.ts +158 -0
- package/app/lib/auto-arrange.ts +147 -0
- package/app/lib/canvas-export.ts +358 -358
- package/app/lib/canvas.ts +625 -564
- package/app/lib/cards.tsx +1361 -916
- package/app/lib/chat.tsx +65 -9
- package/app/lib/code-editor.ts +86 -2
- package/app/lib/context.test.ts +32 -0
- package/app/lib/context.ts +19 -3
- package/app/lib/cursor-sharing.ts +34 -0
- package/app/lib/events.tsx +71 -93
- package/app/lib/export-canvas.ts +287 -0
- package/app/lib/file-card-plugin.ts +148 -148
- package/app/lib/file-modal.tsx +49 -0
- package/app/lib/file-preview.ts +486 -427
- package/app/lib/github-import.test.ts +424 -0
- package/app/lib/initial-route-hydration.test.ts +283 -0
- package/app/lib/initial-route-hydration.ts +202 -0
- package/app/lib/landing-reset.test.ts +99 -0
- package/app/lib/landing-reset.ts +106 -0
- package/app/lib/landing-shell.test.ts +75 -0
- package/app/lib/large-repo-optimization.ts +37 -0
- package/app/lib/layout-snapshots.ts +320 -0
- package/app/lib/loading.test.ts +69 -0
- package/app/lib/loading.tsx +160 -45
- package/app/lib/mount-cleanup.test.ts +52 -0
- package/app/lib/mount-cleanup.ts +34 -0
- package/app/lib/mount-init.test.ts +123 -0
- package/app/lib/mount-init.ts +107 -0
- package/app/lib/mount-lifecycle.test.ts +39 -0
- package/app/lib/mount-lifecycle.ts +12 -0
- package/app/lib/mount-route-wiring.test.ts +87 -0
- package/app/lib/mount-route-wiring.ts +84 -0
- package/app/lib/multi-repo.ts +14 -0
- package/app/lib/onboarding-tutorial.ts +278 -0
- package/app/lib/positions.ts +190 -121
- package/app/lib/recent-commits.test.ts +947 -0
- package/app/lib/recent-commits.ts +227 -0
- package/app/lib/repo-handoff.test.ts +23 -0
- package/app/lib/repo-handoff.ts +16 -0
- package/app/lib/repo-progressive.ts +119 -0
- package/app/lib/repo-select.test.ts +61 -0
- package/app/lib/repo-select.ts +74 -0
- package/app/lib/repo.tsx +1383 -987
- package/app/lib/role.ts +228 -0
- package/app/lib/route-catchall.test.ts +27 -0
- package/app/lib/route-repo-entry.test.ts +95 -0
- package/app/lib/route-repo-entry.ts +36 -0
- package/app/lib/router-contract.test.ts +22 -0
- package/app/lib/router-contract.ts +19 -0
- package/app/lib/shared-layout.test.ts +86 -0
- package/app/lib/shared-layout.ts +82 -0
- package/app/lib/status-bar.test.ts +118 -0
- package/app/lib/status-bar.ts +365 -128
- package/app/lib/sync-controls.test.ts +43 -0
- package/app/lib/sync-controls.tsx +303 -0
- package/app/lib/test-dom.ts +145 -0
- package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
- package/app/lib/transclusion-smoke.test.ts +163 -0
- package/app/lib/tutorial.ts +301 -0
- package/app/lib/version.ts +93 -0
- package/app/lib/viewport-culling.ts +740 -735
- package/app/lib/virtual-files.ts +456 -0
- package/app/lib/webgl-text.ts +189 -0
- package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -482
- package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
- package/app/og-image.png +0 -0
- package/app/page.client.tsx +70 -269
- package/app/page.tsx +15 -16
- package/app/state/machine.js +13 -0
- package/package.json +84 -75
- package/server.ts +10 -0
- package/app/[owner]/[repo]/page.tsx +0 -6
- package/app/[slug]/page.tsx +0 -6
- package/packages/galaxydraw/README.md +0 -296
- package/packages/galaxydraw/banner.png +0 -0
- package/packages/galaxydraw/demo/build-static.ts +0 -100
- package/packages/galaxydraw/demo/client.ts +0 -154
- package/packages/galaxydraw/demo/dist/client.js +0 -8
- package/packages/galaxydraw/demo/index.html +0 -256
- package/packages/galaxydraw/demo/server.ts +0 -96
- package/packages/galaxydraw/dist/index.js +0 -984
- package/packages/galaxydraw/dist/index.js.map +0 -16
- package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
- package/packages/galaxydraw/package.json +0 -49
- package/packages/galaxydraw/perf.test.ts +0 -284
- package/packages/galaxydraw/src/core/cards.ts +0 -435
- package/packages/galaxydraw/src/core/engine.ts +0 -339
- package/packages/galaxydraw/src/core/events.ts +0 -81
- package/packages/galaxydraw/src/core/layout.ts +0 -136
- package/packages/galaxydraw/src/core/minimap.ts +0 -216
- package/packages/galaxydraw/src/core/state.ts +0 -177
- package/packages/galaxydraw/src/core/viewport.ts +0 -106
- package/packages/galaxydraw/src/galaxydraw.css +0 -166
- package/packages/galaxydraw/src/index.ts +0 -40
- package/packages/galaxydraw/tsconfig.json +0 -30
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Virtual Files — Transclusion-based compression for large files
|
|
3
|
+
*
|
|
4
|
+
* Detects repeating content in large files and extracts it into virtual cards.
|
|
5
|
+
* Virtual cards are anchored near the original file and linked on hover.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CanvasContext } from './context';
|
|
9
|
+
import { escapeHtml } from './utils';
|
|
10
|
+
import { jumpToFile } from './canvas';
|
|
11
|
+
|
|
12
|
+
export interface VirtualSegment {
|
|
13
|
+
id: string;
|
|
14
|
+
content: string;
|
|
15
|
+
occurrences: number;
|
|
16
|
+
lineNumbers: number[];
|
|
17
|
+
type: 'prefix' | 'repeating' | 'boilerplate';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface VirtualFile {
|
|
21
|
+
path: string;
|
|
22
|
+
originalPath: string;
|
|
23
|
+
segments: VirtualSegment[];
|
|
24
|
+
compressionRatio: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MIN_LINES = 120;
|
|
28
|
+
const MIN_BYTES = 12_000;
|
|
29
|
+
const MAX_VIRTUAL_FILES = 6;
|
|
30
|
+
const MAX_SEGMENTS_PER_FILE = 2;
|
|
31
|
+
const VIRTUAL_CARD_WIDTH = 320;
|
|
32
|
+
const VIRTUAL_CARD_HEIGHT = 200;
|
|
33
|
+
const VIRTUAL_CARD_GAP_X = 36;
|
|
34
|
+
const VIRTUAL_CARD_GAP_Y = 22;
|
|
35
|
+
|
|
36
|
+
const EXCLUDED_PATH_PATTERNS = [
|
|
37
|
+
/(^|\/)dist\//,
|
|
38
|
+
/(^|\/)build\//,
|
|
39
|
+
/(^|\/)coverage\//,
|
|
40
|
+
/(^|\/)\.docs\//,
|
|
41
|
+
/(^|\/)docs\//,
|
|
42
|
+
/(^|\/)tests?\//,
|
|
43
|
+
/(^|\/)__tests__\//,
|
|
44
|
+
/(^|\/)packages\/galaxydraw\/dist\//,
|
|
45
|
+
/(^|\/)node_modules\//,
|
|
46
|
+
/(^|\/)app\/globals\.css$/,
|
|
47
|
+
/(^|\/)app\/styles\/main\.css$/,
|
|
48
|
+
/(^|\/)app\/layout\.tsx$/,
|
|
49
|
+
/\.test\.[^.]+$/,
|
|
50
|
+
/\.spec\.[^.]+$/,
|
|
51
|
+
/-lock\./,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const DEPRIORITIZED_EXTS = new Set(['css', 'scss', 'less', 'md', 'txt', 'svg', 'json']);
|
|
55
|
+
|
|
56
|
+
// ─── Detection ───────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Analyze file content for repeating patterns.
|
|
60
|
+
*/
|
|
61
|
+
export function detectVirtualSegments(content: string, filePath: string): VirtualSegment[] {
|
|
62
|
+
const lines = content.split('\n');
|
|
63
|
+
const segments: VirtualSegment[] = [];
|
|
64
|
+
|
|
65
|
+
if (lines.length < 50) return segments;
|
|
66
|
+
|
|
67
|
+
segments.push(...detectCommonPrefixes(lines, filePath));
|
|
68
|
+
segments.push(...detectRepeatingBlocks(lines, filePath));
|
|
69
|
+
|
|
70
|
+
segments.sort((a, b) => compressionScore(b) - compressionScore(a));
|
|
71
|
+
|
|
72
|
+
const deduped: VirtualSegment[] = [];
|
|
73
|
+
const seen = new Set<string>();
|
|
74
|
+
for (const segment of segments) {
|
|
75
|
+
const key = `${segment.type}:${segment.content.slice(0, 160)}`;
|
|
76
|
+
if (seen.has(key)) continue;
|
|
77
|
+
seen.add(key);
|
|
78
|
+
deduped.push(segment);
|
|
79
|
+
if (deduped.length >= 5) break;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return deduped;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function compressionScore(segment: VirtualSegment): number {
|
|
86
|
+
const base = segment.content.length * Math.max(segment.occurrences - 1, 1);
|
|
87
|
+
const typeBoost = segment.type === 'repeating' ? 1.35 : 1;
|
|
88
|
+
const occurrenceBoost = Math.min(segment.occurrences / 8, 2.5);
|
|
89
|
+
return Math.round(base * typeBoost * occurrenceBoost);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Detect common line prefixes (e.g. timestamps / log prefixes).
|
|
94
|
+
*/
|
|
95
|
+
function detectCommonPrefixes(lines: string[], filePath: string): VirtualSegment[] {
|
|
96
|
+
const prefixMap = new Map<string, number[]>();
|
|
97
|
+
const MIN_PREFIX_LENGTH = 16;
|
|
98
|
+
const MAX_PREFIX_LENGTH = 80;
|
|
99
|
+
const MIN_OCCURRENCES = 10;
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < lines.length; i++) {
|
|
102
|
+
const line = lines[i];
|
|
103
|
+
if (line.length < MIN_PREFIX_LENGTH) continue;
|
|
104
|
+
|
|
105
|
+
for (let len = MIN_PREFIX_LENGTH; len <= Math.min(MAX_PREFIX_LENGTH, line.length); len += 8) {
|
|
106
|
+
const prefix = line.substring(0, len);
|
|
107
|
+
if (!looksCompressiblePrefix(prefix)) continue;
|
|
108
|
+
if (!prefixMap.has(prefix)) prefixMap.set(prefix, []);
|
|
109
|
+
prefixMap.get(prefix)!.push(i);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const segments: VirtualSegment[] = [];
|
|
114
|
+
let idCounter = 0;
|
|
115
|
+
|
|
116
|
+
for (const [prefix, lineNumbers] of prefixMap.entries()) {
|
|
117
|
+
if (lineNumbers.length < MIN_OCCURRENCES) continue;
|
|
118
|
+
segments.push({
|
|
119
|
+
id: `virtual-${filePath.replace(/[^a-z0-9]/gi, '-')}-prefix-${idCounter++}`,
|
|
120
|
+
content: prefix,
|
|
121
|
+
occurrences: lineNumbers.length,
|
|
122
|
+
lineNumbers,
|
|
123
|
+
type: 'prefix',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return segments;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function looksCompressiblePrefix(prefix: string): boolean {
|
|
131
|
+
// Avoid generating cards for boring indentation or very low-signal prefixes.
|
|
132
|
+
const trimmed = prefix.trim();
|
|
133
|
+
if (trimmed.length < 12) return false;
|
|
134
|
+
if (/^[\[{(<\-_=:.\s]+$/.test(prefix)) return false;
|
|
135
|
+
if (/^[A-Z_-]+:?$/.test(trimmed)) return false;
|
|
136
|
+
if (/^[a-z0-9_-]+$/i.test(trimmed) && trimmed.length < 18) return false;
|
|
137
|
+
const alphaCount = (trimmed.match(/[A-Za-z]/g) || []).length;
|
|
138
|
+
if (alphaCount < 4) return false;
|
|
139
|
+
return /[A-Za-z0-9]/.test(prefix);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Detect repeating blocks of text.
|
|
144
|
+
*/
|
|
145
|
+
function detectRepeatingBlocks(lines: string[], filePath: string): VirtualSegment[] {
|
|
146
|
+
const blockMap = new Map<string, number[]>();
|
|
147
|
+
const BLOCK_SIZE = 4;
|
|
148
|
+
const MIN_OCCURRENCES = 3;
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i <= lines.length - BLOCK_SIZE; i++) {
|
|
151
|
+
const block = lines.slice(i, i + BLOCK_SIZE).join('\n');
|
|
152
|
+
if (block.length < 80) continue;
|
|
153
|
+
if (!blockMap.has(block)) blockMap.set(block, []);
|
|
154
|
+
blockMap.get(block)!.push(i);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const segments: VirtualSegment[] = [];
|
|
158
|
+
let idCounter = 0;
|
|
159
|
+
|
|
160
|
+
for (const [block, lineNumbers] of blockMap.entries()) {
|
|
161
|
+
if (lineNumbers.length < MIN_OCCURRENCES) continue;
|
|
162
|
+
segments.push({
|
|
163
|
+
id: `virtual-${filePath.replace(/[^a-z0-9]/gi, '-')}-block-${idCounter++}`,
|
|
164
|
+
content: block,
|
|
165
|
+
occurrences: lineNumbers.length,
|
|
166
|
+
lineNumbers,
|
|
167
|
+
type: 'repeating',
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return segments;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── Lifecycle / placement ───────────────────────────────
|
|
175
|
+
|
|
176
|
+
export function clearVirtualCards(ctx?: CanvasContext): void {
|
|
177
|
+
document.querySelectorAll('.virtual-card').forEach((card) => card.remove());
|
|
178
|
+
const overlay = (ctx?.svgOverlay || document.getElementById('connectionsOverlay')) as SVGSVGElement | null;
|
|
179
|
+
if (overlay) {
|
|
180
|
+
overlay.querySelectorAll('.virtual-connection').forEach((line) => line.remove());
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getOriginalPlacement(ctx: CanvasContext, originalFilePath: string) {
|
|
185
|
+
const mounted = ctx.fileCards.get(originalFilePath);
|
|
186
|
+
if (mounted) {
|
|
187
|
+
return {
|
|
188
|
+
x: parseFloat(mounted.style.left) || 0,
|
|
189
|
+
y: parseFloat(mounted.style.top) || 0,
|
|
190
|
+
w: mounted.offsetWidth || 580,
|
|
191
|
+
h: mounted.offsetHeight || 700,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const deferred = ctx.deferredCards.get(originalFilePath);
|
|
196
|
+
if (deferred) {
|
|
197
|
+
return {
|
|
198
|
+
x: deferred.x,
|
|
199
|
+
y: deferred.y,
|
|
200
|
+
w: deferred.size?.width || 580,
|
|
201
|
+
h: deferred.size?.height || 700,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function shouldCreateVirtualCards(file: any): boolean {
|
|
209
|
+
if (!file?.content || typeof file.content !== 'string') return false;
|
|
210
|
+
if (file.isBinary || file.type !== 'file') return false;
|
|
211
|
+
const normalizedPath = String(file.path || '').replace(/\\/g, '/');
|
|
212
|
+
if (EXCLUDED_PATH_PATTERNS.some((pattern) => pattern.test(normalizedPath))) return false;
|
|
213
|
+
const lineCount = file.lines || file.content.split('\n').length;
|
|
214
|
+
const byteSize = file.size || file.content.length;
|
|
215
|
+
return lineCount >= MIN_LINES || byteSize >= MIN_BYTES;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function getFilePriorityScore(file: any): number {
|
|
219
|
+
const normalizedPath = String(file.path || '').replace(/\\/g, '/');
|
|
220
|
+
const ext = String(file.ext || '').toLowerCase();
|
|
221
|
+
let score = 1;
|
|
222
|
+
|
|
223
|
+
if (normalizedPath.startsWith('app/') || normalizedPath.startsWith('src/')) score += 1.4;
|
|
224
|
+
if (normalizedPath.includes('/lib/')) score += 0.8;
|
|
225
|
+
if (normalizedPath.includes('/api/')) score += 0.45;
|
|
226
|
+
if (normalizedPath.includes('/components/')) score += 0.45;
|
|
227
|
+
if (normalizedPath.includes('/route.')) score += 0.2;
|
|
228
|
+
|
|
229
|
+
if (DEPRIORITIZED_EXTS.has(ext)) score -= 0.9;
|
|
230
|
+
if (ext === 'ts' || ext === 'tsx' || ext === 'js' || ext === 'jsx') score += 0.75;
|
|
231
|
+
|
|
232
|
+
return Math.max(score, 0.15);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getEligibleVirtualFiles(files: any[]): any[] {
|
|
236
|
+
return files
|
|
237
|
+
.filter(shouldCreateVirtualCards)
|
|
238
|
+
.map((file) => {
|
|
239
|
+
const segments = detectVirtualSegments(file.content, file.path)
|
|
240
|
+
.filter((seg) => seg.type === 'repeating' || seg.occurrences >= 12)
|
|
241
|
+
.slice(0, 3);
|
|
242
|
+
const baseScore = segments.reduce((sum, seg) => sum + compressionScore(seg), 0);
|
|
243
|
+
const score = Math.round(baseScore * getFilePriorityScore(file));
|
|
244
|
+
return { file, segments, score };
|
|
245
|
+
})
|
|
246
|
+
.filter((entry) => entry.segments.length > 0 && entry.score > 900)
|
|
247
|
+
.sort((a, b) => b.score - a.score)
|
|
248
|
+
.slice(0, MAX_VIRTUAL_FILES);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ─── Virtual Card Creation ───────────────────────────────
|
|
252
|
+
|
|
253
|
+
export function createVirtualCard(
|
|
254
|
+
ctx: CanvasContext,
|
|
255
|
+
segment: VirtualSegment,
|
|
256
|
+
originalFilePath: string,
|
|
257
|
+
x: number,
|
|
258
|
+
y: number,
|
|
259
|
+
): HTMLElement {
|
|
260
|
+
const card = document.createElement('div');
|
|
261
|
+
card.className = 'file-card virtual-card';
|
|
262
|
+
card.dataset.virtual = 'true';
|
|
263
|
+
card.dataset.originalPath = originalFilePath;
|
|
264
|
+
card.dataset.segmentId = segment.id;
|
|
265
|
+
card.dataset.path = `${originalFilePath}::${segment.id}`;
|
|
266
|
+
card.style.left = `${x}px`;
|
|
267
|
+
card.style.top = `${y}px`;
|
|
268
|
+
|
|
269
|
+
const typeIcon = segment.type === 'prefix' ? '🔖' : segment.type === 'repeating' ? '🔁' : '📋';
|
|
270
|
+
const compressionRatio = Math.round((1 - 1 / Math.max(segment.occurrences, 1)) * 100);
|
|
271
|
+
const title = segment.type === 'prefix' ? 'Shared Prefix' : 'Repeated Block';
|
|
272
|
+
const fileName = originalFilePath.split('/').pop() || originalFilePath;
|
|
273
|
+
const linePreview = segment.lineNumbers.slice(0, 4).map((n) => n + 1).join(', ');
|
|
274
|
+
const lineSummary = segment.lineNumbers.length === 1
|
|
275
|
+
? `line ${segment.lineNumbers[0] + 1}`
|
|
276
|
+
: `lines ${linePreview}${segment.lineNumbers.length > 4 ? ', …' : ''}`;
|
|
277
|
+
|
|
278
|
+
card.innerHTML = `
|
|
279
|
+
<div class="card-header" style="background: linear-gradient(135deg, rgba(124,58,237,0.22), rgba(59,130,246,0.22)); border-bottom: 1px solid var(--border-primary); display:flex; align-items:center; gap:8px; padding:10px 12px;">
|
|
280
|
+
<span style="font-size: 14px;">${typeIcon}</span>
|
|
281
|
+
<div style="flex:1; min-width:0;">
|
|
282
|
+
<div style="font-weight:600; font-size:11px; color:var(--text-primary);">${title}</div>
|
|
283
|
+
<div style="font-size:10px; color:var(--text-muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${escapeHtml(fileName)} · ${lineSummary}</div>
|
|
284
|
+
</div>
|
|
285
|
+
<span style="font-size:10px; color: var(--accent-primary);">-${compressionRatio}%</span>
|
|
286
|
+
</div>
|
|
287
|
+
<div class="card-body" style="padding: 12px; font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--text-muted); overflow: hidden;">
|
|
288
|
+
<div style="display:flex; justify-content:space-between; margin-bottom:8px; color:var(--text-primary); gap:8px;">
|
|
289
|
+
<span>${segment.occurrences} occurrences</span>
|
|
290
|
+
<span style="text-align:right; opacity:0.9;">click to jump to source</span>
|
|
291
|
+
</div>
|
|
292
|
+
<div style="background: rgba(0,0,0,0.3); padding: 8px; border-radius: 4px; max-height: 120px; overflow: hidden;">
|
|
293
|
+
<code style="white-space: pre-wrap; word-break: break-word; color: #94a3b8;">${escapeHtml(segment.content.substring(0, 340))}${segment.content.length > 340 ? '…' : ''}</code>
|
|
294
|
+
</div>
|
|
295
|
+
<div style="margin-top:8px; font-size:10px; opacity:0.8;">↗ ${escapeHtml(originalFilePath)} · ${lineSummary}</div>
|
|
296
|
+
</div>
|
|
297
|
+
`;
|
|
298
|
+
|
|
299
|
+
(card as HTMLElement).style.cssText += `
|
|
300
|
+
position: absolute;
|
|
301
|
+
width: ${VIRTUAL_CARD_WIDTH}px;
|
|
302
|
+
min-height: ${VIRTUAL_CARD_HEIGHT}px;
|
|
303
|
+
background: color-mix(in srgb, var(--bg-card) 92%, #7c3aed 8%);
|
|
304
|
+
border: 1px solid rgba(124, 58, 237, 0.45);
|
|
305
|
+
border-radius: 10px;
|
|
306
|
+
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.28);
|
|
307
|
+
cursor: pointer;
|
|
308
|
+
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
|
309
|
+
z-index: 12;
|
|
310
|
+
`;
|
|
311
|
+
|
|
312
|
+
card.addEventListener('mouseenter', () => {
|
|
313
|
+
highlightConnections(ctx, segment.id, originalFilePath);
|
|
314
|
+
card.style.transform = 'translateY(-2px)';
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
card.addEventListener('mouseleave', () => {
|
|
318
|
+
clearConnectionHighlights(ctx);
|
|
319
|
+
card.style.transform = '';
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
card.title = `Jump to ${originalFilePath}`;
|
|
323
|
+
|
|
324
|
+
card.addEventListener('click', () => {
|
|
325
|
+
jumpToFile(ctx, originalFilePath);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return card;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ─── Main Integration ────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
export async function processFileForVirtualCards(
|
|
334
|
+
ctx: CanvasContext,
|
|
335
|
+
filePath: string,
|
|
336
|
+
content: string,
|
|
337
|
+
segmentOffset = 0,
|
|
338
|
+
): Promise<number> {
|
|
339
|
+
const placement = getOriginalPlacement(ctx, filePath);
|
|
340
|
+
if (!placement) return 0;
|
|
341
|
+
|
|
342
|
+
const segments = detectVirtualSegments(content, filePath).slice(0, MAX_SEGMENTS_PER_FILE);
|
|
343
|
+
if (segments.length === 0) return 0;
|
|
344
|
+
|
|
345
|
+
const canvasContent = document.getElementById('canvasContent');
|
|
346
|
+
if (!canvasContent) return 0;
|
|
347
|
+
|
|
348
|
+
let created = 0;
|
|
349
|
+
for (let i = 0; i < segments.length; i++) {
|
|
350
|
+
const segment = segments[i];
|
|
351
|
+
const x = placement.x + placement.w + VIRTUAL_CARD_GAP_X;
|
|
352
|
+
const y = placement.y + (segmentOffset + i) * (VIRTUAL_CARD_HEIGHT + VIRTUAL_CARD_GAP_Y);
|
|
353
|
+
const card = createVirtualCard(ctx, segment, filePath, x, y);
|
|
354
|
+
canvasContent.appendChild(card);
|
|
355
|
+
created++;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return created;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export async function processVirtualFileSet(ctx: CanvasContext, files: any[]): Promise<number> {
|
|
362
|
+
clearVirtualCards(ctx);
|
|
363
|
+
|
|
364
|
+
const eligible = getEligibleVirtualFiles(files);
|
|
365
|
+
(window as any).__virtualCandidates = eligible.slice(0, 12).map((entry) => ({
|
|
366
|
+
path: entry.file.path,
|
|
367
|
+
score: entry.score,
|
|
368
|
+
segments: entry.segments.length,
|
|
369
|
+
}));
|
|
370
|
+
if (eligible.length === 0) return 0;
|
|
371
|
+
|
|
372
|
+
let created = 0;
|
|
373
|
+
let segmentOffset = 0;
|
|
374
|
+
for (const entry of eligible) {
|
|
375
|
+
created += await processFileForVirtualCards(
|
|
376
|
+
ctx,
|
|
377
|
+
entry.file.path,
|
|
378
|
+
entry.file.content,
|
|
379
|
+
segmentOffset,
|
|
380
|
+
);
|
|
381
|
+
segmentOffset += MAX_SEGMENTS_PER_FILE;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return created;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ─── Connection Highlighting ─────────────────────────────
|
|
388
|
+
|
|
389
|
+
function highlightConnections(
|
|
390
|
+
ctx: CanvasContext,
|
|
391
|
+
segmentId: string,
|
|
392
|
+
originalFilePath: string,
|
|
393
|
+
): void {
|
|
394
|
+
const virtualCard = document.querySelector(`[data-segment-id="${segmentId}"]`) as HTMLElement | null;
|
|
395
|
+
if (virtualCard) {
|
|
396
|
+
virtualCard.style.boxShadow = '0 0 24px rgba(124,58,237,0.45)';
|
|
397
|
+
virtualCard.style.borderColor = 'var(--accent-primary)';
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const originalCard = Array.from(ctx.fileCards.values()).find(
|
|
401
|
+
(card) => card.dataset.path === originalFilePath,
|
|
402
|
+
);
|
|
403
|
+
if (originalCard) {
|
|
404
|
+
(originalCard as HTMLElement).style.boxShadow = '0 0 24px rgba(124,58,237,0.45)';
|
|
405
|
+
(originalCard as HTMLElement).style.borderColor = 'var(--accent-primary)';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (virtualCard && originalCard) {
|
|
409
|
+
drawConnectionLine(virtualCard, originalCard as HTMLElement);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function clearConnectionHighlights(ctx: CanvasContext): void {
|
|
414
|
+
document.querySelectorAll('.virtual-card').forEach((card) => {
|
|
415
|
+
(card as HTMLElement).style.boxShadow = '';
|
|
416
|
+
(card as HTMLElement).style.borderColor = '';
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
ctx.fileCards.forEach((card) => {
|
|
420
|
+
card.style.boxShadow = '';
|
|
421
|
+
card.style.borderColor = '';
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
const overlay = (ctx.svgOverlay || document.getElementById('connectionsOverlay')) as SVGSVGElement | null;
|
|
425
|
+
if (overlay) {
|
|
426
|
+
overlay.querySelectorAll('.virtual-connection').forEach((line) => line.remove());
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function drawConnectionLine(from: HTMLElement, to: HTMLElement): void {
|
|
431
|
+
const overlay = document.getElementById('connectionsOverlay') as SVGSVGElement | null;
|
|
432
|
+
if (!overlay) return;
|
|
433
|
+
|
|
434
|
+
overlay.querySelectorAll('.virtual-connection').forEach((line) => line.remove());
|
|
435
|
+
|
|
436
|
+
const fromRect = from.getBoundingClientRect();
|
|
437
|
+
const toRect = to.getBoundingClientRect();
|
|
438
|
+
const viewport = overlay.getBoundingClientRect();
|
|
439
|
+
|
|
440
|
+
const x1 = fromRect.left + fromRect.width / 2 - viewport.left;
|
|
441
|
+
const y1 = fromRect.top + fromRect.height / 2 - viewport.top;
|
|
442
|
+
const x2 = toRect.left + toRect.width / 2 - viewport.left;
|
|
443
|
+
const y2 = toRect.top + toRect.height / 2 - viewport.top;
|
|
444
|
+
|
|
445
|
+
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
446
|
+
line.setAttribute('x1', String(x1));
|
|
447
|
+
line.setAttribute('y1', String(y1));
|
|
448
|
+
line.setAttribute('x2', String(x2));
|
|
449
|
+
line.setAttribute('y2', String(y2));
|
|
450
|
+
line.setAttribute('class', 'virtual-connection');
|
|
451
|
+
line.setAttribute('stroke', 'var(--accent)');
|
|
452
|
+
line.setAttribute('stroke-width', '1.5');
|
|
453
|
+
line.setAttribute('stroke-dasharray', '4 4');
|
|
454
|
+
line.setAttribute('opacity', '0.55');
|
|
455
|
+
overlay.appendChild(line);
|
|
456
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebGLTextRenderer — Pixi.js GPU-accelerated code text rendering
|
|
3
|
+
*
|
|
4
|
+
* Drop-in replacement for CanvasTextRenderer with 10x performance improvement.
|
|
5
|
+
* Renders 5000+ files at 60fps vs 1000 files thermal throttling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as PIXI from 'pixi.js';
|
|
9
|
+
import type { CanvasTextOptions } from './canvas-text';
|
|
10
|
+
|
|
11
|
+
export interface WebGLTextOptions extends CanvasTextOptions {
|
|
12
|
+
fontSize?: number;
|
|
13
|
+
fontFamily?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class WebGLTextRenderer {
|
|
17
|
+
private app: PIXI.Application;
|
|
18
|
+
private container: PIXI.Container;
|
|
19
|
+
private lines: PIXI.Text[] = [];
|
|
20
|
+
private lineNumbers: PIXI.Text[] = [];
|
|
21
|
+
|
|
22
|
+
private options: WebGLTextOptions;
|
|
23
|
+
private lineHeight: number = 20;
|
|
24
|
+
private scrollTop: number = 0;
|
|
25
|
+
private viewportHeight: number = 0;
|
|
26
|
+
private viewportWidth: number = 0;
|
|
27
|
+
|
|
28
|
+
private _highlightedHunkIdx: number = -1;
|
|
29
|
+
private hunkRanges: { startIdx: number; endIdx: number; type: 'add' | 'del' }[] = [];
|
|
30
|
+
|
|
31
|
+
constructor(container: HTMLElement, options: WebGLTextOptions) {
|
|
32
|
+
this.options = options;
|
|
33
|
+
this.lineHeight = options.fontSize ? options.fontSize + 8 : 20;
|
|
34
|
+
|
|
35
|
+
// Initialize Pixi Application
|
|
36
|
+
this.viewportWidth = container.clientWidth;
|
|
37
|
+
this.viewportHeight = container.clientHeight;
|
|
38
|
+
|
|
39
|
+
this.app = new PIXI.Application({
|
|
40
|
+
width: this.viewportWidth,
|
|
41
|
+
height: this.viewportHeight,
|
|
42
|
+
backgroundColor: 0x0a0a0f,
|
|
43
|
+
resolution: window.devicePixelRatio || 1,
|
|
44
|
+
antialias: false,
|
|
45
|
+
autoDensity: true,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
container.appendChild(this.app.canvas);
|
|
49
|
+
this.app.canvas.style.cssText = 'position: absolute; top: 0; left: 0; width: 100%; height: 100%;';
|
|
50
|
+
|
|
51
|
+
this.container = new PIXI.Container();
|
|
52
|
+
this.app.stage.addChild(this.container);
|
|
53
|
+
|
|
54
|
+
// Parse content into lines
|
|
55
|
+
this.setContent(options.content || '', options);
|
|
56
|
+
|
|
57
|
+
// Handle resize
|
|
58
|
+
const resizeObserver = new ResizeObserver(() => this.handleResize());
|
|
59
|
+
resizeObserver.observe(container);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private handleResize(): void {
|
|
63
|
+
const parent = this.app.canvas.parentElement;
|
|
64
|
+
if (!parent) return;
|
|
65
|
+
|
|
66
|
+
this.viewportWidth = parent.clientWidth;
|
|
67
|
+
this.viewportHeight = parent.clientHeight;
|
|
68
|
+
|
|
69
|
+
this.app.renderer.resize(this.viewportWidth, this.viewportHeight);
|
|
70
|
+
this.app.stage.hitArea = new PIXI.Rectangle(0, 0, this.viewportWidth, this.viewportHeight);
|
|
71
|
+
|
|
72
|
+
this.render();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private setContent(content: string, options: WebGLTextOptions): void {
|
|
76
|
+
// Clear existing
|
|
77
|
+
this.container.removeChildren();
|
|
78
|
+
this.lines = [];
|
|
79
|
+
this.lineNumbers = [];
|
|
80
|
+
|
|
81
|
+
const lines = content.split('\n');
|
|
82
|
+
this.hunkRanges = [];
|
|
83
|
+
|
|
84
|
+
// Calculate hunk ranges from diff info
|
|
85
|
+
if (options.addedLines || options.deletedBeforeLine) {
|
|
86
|
+
let idx = 0;
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
if (options.addedLines?.has(idx + 1)) {
|
|
89
|
+
this.hunkRanges.push({ startIdx: idx, endIdx: idx + 1, type: 'add' });
|
|
90
|
+
}
|
|
91
|
+
idx++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Calculate line number width
|
|
96
|
+
const numDigits = Math.max(3, lines.length.toString().length);
|
|
97
|
+
this.lineNumWidth = numDigits * 8 + 20;
|
|
98
|
+
|
|
99
|
+
// Create line numbers and text
|
|
100
|
+
for (let i = 0; i < lines.length; i++) {
|
|
101
|
+
// Line number
|
|
102
|
+
const lineNum = new PIXI.Text((i + 1).toString(), {
|
|
103
|
+
fontFamily: 'JetBrains Mono, monospace',
|
|
104
|
+
fontSize: 11,
|
|
105
|
+
fill: 0x64748b,
|
|
106
|
+
});
|
|
107
|
+
lineNum.x = 6;
|
|
108
|
+
lineNum.y = 6 + (i * this.lineHeight);
|
|
109
|
+
this.container.addChild(lineNum);
|
|
110
|
+
this.lineNumbers.push(lineNum);
|
|
111
|
+
|
|
112
|
+
// Code line
|
|
113
|
+
const lineText = new PIXI.Text(lines[i], {
|
|
114
|
+
fontFamily: 'JetBrains Mono, monospace',
|
|
115
|
+
fontSize: options.fontSize || 12,
|
|
116
|
+
fill: 0x94a3b8,
|
|
117
|
+
});
|
|
118
|
+
lineText.x = this.lineNumWidth + 12;
|
|
119
|
+
lineText.y = 6 + (i * this.lineHeight);
|
|
120
|
+
this.container.addChild(lineText);
|
|
121
|
+
this.lines.push(lineText);
|
|
122
|
+
|
|
123
|
+
// Apply diff coloring
|
|
124
|
+
if (options.addedLines?.has(i + 1)) {
|
|
125
|
+
lineText.style.fill = 0x22c55e;
|
|
126
|
+
} else if (options.deletedBeforeLine?.has(i + 1)) {
|
|
127
|
+
lineText.style.fill = 0xef4444;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.render();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
scrollTo(line: number): void {
|
|
135
|
+
const targetY = -(line * this.lineHeight) + (this.viewportHeight / 2);
|
|
136
|
+
this.container.y = Math.min(0, targetY);
|
|
137
|
+
this.scrollTop = -this.container.y;
|
|
138
|
+
this.render();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
setZoom(zoom: number): void {
|
|
142
|
+
this.container.scale.set(zoom);
|
|
143
|
+
this.render();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
highlightHunk(hunkIndex: number): void {
|
|
147
|
+
this._highlightedHunkIdx = hunkIndex;
|
|
148
|
+
|
|
149
|
+
if (hunkIndex >= 0 && hunkIndex < this.hunkRanges.length) {
|
|
150
|
+
const hunk = this.hunkRanges[hunkIndex];
|
|
151
|
+
const highlight = new PIXI.Graphics();
|
|
152
|
+
highlight.beginFill(0x6366f1, 0.2);
|
|
153
|
+
highlight.drawRect(0, hunk.startIdx * this.lineHeight, this.viewportWidth, (hunk.endIdx - hunk.startIdx) * this.lineHeight);
|
|
154
|
+
highlight.endFill();
|
|
155
|
+
this.container.addChild(highlight);
|
|
156
|
+
|
|
157
|
+
// Fade out animation
|
|
158
|
+
const animate = () => {
|
|
159
|
+
highlight.alpha -= 0.05;
|
|
160
|
+
if (highlight.alpha > 0) {
|
|
161
|
+
requestAnimationFrame(animate);
|
|
162
|
+
} else {
|
|
163
|
+
this.container.removeChild(highlight);
|
|
164
|
+
highlight.destroy();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
animate();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private render(): void {
|
|
172
|
+
// Viewport culling - hide lines outside viewport
|
|
173
|
+
const visibleStart = Math.floor(-this.container.y / this.lineHeight);
|
|
174
|
+
const visibleEnd = visibleStart + Math.ceil(this.viewportHeight / this.lineHeight) + 5;
|
|
175
|
+
|
|
176
|
+
this.lines.forEach((line, i) => {
|
|
177
|
+
line.visible = i >= visibleStart && i <= visibleEnd;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
this.lineNumbers.forEach((num, i) => {
|
|
181
|
+
num.visible = i >= visibleStart && i <= visibleEnd;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
destroy(): void {
|
|
186
|
+
this.app.destroy(true, { children: true, texture: true });
|
|
187
|
+
this.app.canvas.remove();
|
|
188
|
+
}
|
|
189
|
+
}
|