gitmaps 1.0.0

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.
Files changed (121) hide show
  1. package/README.md +167 -0
  2. package/app/api/auth/favorites/route.ts +56 -0
  3. package/app/api/auth/github/callback/route.ts +103 -0
  4. package/app/api/auth/github/route.ts +32 -0
  5. package/app/api/auth/me/route.ts +52 -0
  6. package/app/api/auth/positions/route.ts +50 -0
  7. package/app/api/chat/route.ts +101 -0
  8. package/app/api/connections/route.ts +72 -0
  9. package/app/api/github/repos/route.ts +111 -0
  10. package/app/api/positions/route.ts +80 -0
  11. package/app/api/repo/branch-diff/route.ts +201 -0
  12. package/app/api/repo/branches/route.ts +53 -0
  13. package/app/api/repo/browse/route.ts +55 -0
  14. package/app/api/repo/clone/route.ts +78 -0
  15. package/app/api/repo/clone-stream/route.ts +131 -0
  16. package/app/api/repo/file-content/route.ts +28 -0
  17. package/app/api/repo/file-delete/route.ts +62 -0
  18. package/app/api/repo/file-history/route.ts +45 -0
  19. package/app/api/repo/file-rename/route.ts +83 -0
  20. package/app/api/repo/file-save/route.ts +45 -0
  21. package/app/api/repo/files/route.ts +169 -0
  22. package/app/api/repo/git-blame/route.ts +86 -0
  23. package/app/api/repo/git-commit/route.ts +40 -0
  24. package/app/api/repo/git-heatmap/route.ts +55 -0
  25. package/app/api/repo/imports/route.ts +154 -0
  26. package/app/api/repo/load/route.ts +56 -0
  27. package/app/api/repo/mode/route.ts +14 -0
  28. package/app/api/repo/search/route.ts +127 -0
  29. package/app/api/repo/tree/route.ts +104 -0
  30. package/app/api/repo/upload/route.ts +53 -0
  31. package/app/api/repo/validate-path.ts +53 -0
  32. package/app/canvas_users.db +0 -0
  33. package/app/canvas_users.db-shm +0 -0
  34. package/app/canvas_users.db-wal +0 -0
  35. package/app/globals.css +7899 -0
  36. package/app/layout.tsx +493 -0
  37. package/app/lib/auth.ts +193 -0
  38. package/app/lib/auto-save.ts +137 -0
  39. package/app/lib/branch-compare.ts +443 -0
  40. package/app/lib/breadcrumbs.ts +170 -0
  41. package/app/lib/canvas-export.ts +358 -0
  42. package/app/lib/canvas-text.ts +912 -0
  43. package/app/lib/canvas.ts +564 -0
  44. package/app/lib/card-arrangement.ts +188 -0
  45. package/app/lib/card-context-menu.tsx +453 -0
  46. package/app/lib/card-diff-markers.ts +270 -0
  47. package/app/lib/card-expand.ts +189 -0
  48. package/app/lib/card-groups.ts +246 -0
  49. package/app/lib/cards.tsx +914 -0
  50. package/app/lib/chat.tsx +308 -0
  51. package/app/lib/code-editor.ts +508 -0
  52. package/app/lib/command-palette.ts +262 -0
  53. package/app/lib/connections.tsx +1037 -0
  54. package/app/lib/context.ts +94 -0
  55. package/app/lib/cursor-sharing.ts +281 -0
  56. package/app/lib/dependency-graph.ts +438 -0
  57. package/app/lib/events.tsx +1747 -0
  58. package/app/lib/file-card-plugin.ts +134 -0
  59. package/app/lib/file-modal.tsx +849 -0
  60. package/app/lib/file-preview.ts +400 -0
  61. package/app/lib/file-tabs.ts +318 -0
  62. package/app/lib/galaxydraw-bridge.ts +477 -0
  63. package/app/lib/galaxydraw.test.ts +229 -0
  64. package/app/lib/global-search.ts +264 -0
  65. package/app/lib/goto-definition.ts +224 -0
  66. package/app/lib/heatmap.ts +178 -0
  67. package/app/lib/hidden-files.tsx +222 -0
  68. package/app/lib/layers.ts +0 -0
  69. package/app/lib/layers.tsx +365 -0
  70. package/app/lib/loading.tsx +45 -0
  71. package/app/lib/multi-repo.ts +286 -0
  72. package/app/lib/new-file-dialog.tsx +230 -0
  73. package/app/lib/onboarding.tsx +213 -0
  74. package/app/lib/perf-overlay.ts +360 -0
  75. package/app/lib/positions.ts +176 -0
  76. package/app/lib/pr-review.ts +374 -0
  77. package/app/lib/production-mode.ts +47 -0
  78. package/app/lib/repo.tsx +977 -0
  79. package/app/lib/settings-modal.tsx +374 -0
  80. package/app/lib/settings.ts +97 -0
  81. package/app/lib/shortcuts-panel.ts +141 -0
  82. package/app/lib/status-bar.ts +128 -0
  83. package/app/lib/symbol-outline.ts +212 -0
  84. package/app/lib/syntax.ts +177 -0
  85. package/app/lib/tab-diff.ts +238 -0
  86. package/app/lib/user.tsx +133 -0
  87. package/app/lib/utils.ts +78 -0
  88. package/app/lib/viewport-culling.ts +728 -0
  89. package/app/page.client.tsx +215 -0
  90. package/app/page.tsx +291 -0
  91. package/app/state/machine.js +196 -0
  92. package/app/styles/main.css +2168 -0
  93. package/banner.png +0 -0
  94. package/cli.ts +44 -0
  95. package/package.json +75 -0
  96. package/packages/galaxydraw/README.md +296 -0
  97. package/packages/galaxydraw/banner.png +0 -0
  98. package/packages/galaxydraw/demo/build-static.ts +100 -0
  99. package/packages/galaxydraw/demo/client.ts +154 -0
  100. package/packages/galaxydraw/demo/dist/client.js +8 -0
  101. package/packages/galaxydraw/demo/index.html +256 -0
  102. package/packages/galaxydraw/demo/server.ts +96 -0
  103. package/packages/galaxydraw/dist/index.js +984 -0
  104. package/packages/galaxydraw/dist/index.js.map +16 -0
  105. package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
  106. package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
  107. package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
  108. package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
  109. package/packages/galaxydraw/package.json +49 -0
  110. package/packages/galaxydraw/perf.test.ts +284 -0
  111. package/packages/galaxydraw/src/core/cards.ts +435 -0
  112. package/packages/galaxydraw/src/core/engine.ts +339 -0
  113. package/packages/galaxydraw/src/core/events.ts +81 -0
  114. package/packages/galaxydraw/src/core/layout.ts +136 -0
  115. package/packages/galaxydraw/src/core/minimap.ts +216 -0
  116. package/packages/galaxydraw/src/core/state.ts +177 -0
  117. package/packages/galaxydraw/src/core/viewport.ts +106 -0
  118. package/packages/galaxydraw/src/galaxydraw.css +166 -0
  119. package/packages/galaxydraw/src/index.ts +40 -0
  120. package/packages/galaxydraw/tsconfig.json +30 -0
  121. package/server.ts +62 -0
@@ -0,0 +1,508 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * CodeMirror editor wrapper for the file modal.
4
+ * Creates a CodeMirror 6 editor instance with syntax highlighting,
5
+ * dark theme, and integration with the save/commit workflow.
6
+ */
7
+ import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter, drawSelection, rectangularSelection, highlightSpecialChars, ViewPlugin, ViewUpdate } from '@codemirror/view';
8
+ import { EditorState, Compartment } from '@codemirror/state';
9
+ import { syntaxHighlighting, defaultHighlightStyle, indentOnInput, bracketMatching, foldGutter, foldKeymap, HighlightStyle } from '@codemirror/language';
10
+ import { defaultKeymap, indentWithTab, history, historyKeymap } from '@codemirror/commands';
11
+ import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
12
+ import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
13
+ import { tags as t } from '@lezer/highlight';
14
+
15
+ // Language imports
16
+ import { javascript } from '@codemirror/lang-javascript';
17
+ import { css } from '@codemirror/lang-css';
18
+ import { html } from '@codemirror/lang-html';
19
+ import { json } from '@codemirror/lang-json';
20
+ import { markdown } from '@codemirror/lang-markdown';
21
+ import { python } from '@codemirror/lang-python';
22
+ import { yaml } from '@codemirror/lang-yaml';
23
+
24
+ // ─── Dark theme matching GitMaps ────────────────────────
25
+ const gitMapsDarkTheme = EditorView.theme({
26
+ '&': {
27
+ backgroundColor: 'transparent',
28
+ color: '#e2e8f0',
29
+ fontSize: '13px',
30
+ fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
31
+ },
32
+ '.cm-content': {
33
+ caretColor: '#22c55e',
34
+ padding: '8px 0',
35
+ },
36
+ '.cm-cursor, .cm-dropCursor': {
37
+ borderLeftColor: '#22c55e',
38
+ borderLeftWidth: '2px',
39
+ },
40
+ '&.cm-focused .cm-selectionBackground, .cm-selectionBackground': {
41
+ backgroundColor: 'rgba(34, 197, 94, 0.15) !important',
42
+ },
43
+ '.cm-activeLine': {
44
+ backgroundColor: 'rgba(255, 255, 255, 0.03)',
45
+ },
46
+ '.cm-activeLineGutter': {
47
+ backgroundColor: 'rgba(255, 255, 255, 0.05)',
48
+ },
49
+ '.cm-gutters': {
50
+ backgroundColor: 'transparent',
51
+ color: 'rgba(148, 163, 184, 0.4)',
52
+ border: 'none',
53
+ borderRight: '1px solid rgba(255, 255, 255, 0.06)',
54
+ minWidth: '40px',
55
+ },
56
+ '.cm-gutter.cm-lineNumbers .cm-gutterElement': {
57
+ padding: '0 8px 0 4px',
58
+ minWidth: '32px',
59
+ },
60
+ '.cm-foldGutter .cm-gutterElement': {
61
+ padding: '0 4px',
62
+ color: 'rgba(148, 163, 184, 0.3)',
63
+ cursor: 'pointer',
64
+ },
65
+ '.cm-foldGutter .cm-gutterElement:hover': {
66
+ color: '#22c55e',
67
+ },
68
+ '.cm-matchingBracket': {
69
+ backgroundColor: 'rgba(34, 197, 94, 0.2)',
70
+ outline: '1px solid rgba(34, 197, 94, 0.4)',
71
+ },
72
+ '.cm-searchMatch': {
73
+ backgroundColor: 'rgba(250, 204, 21, 0.25)',
74
+ outline: '1px solid rgba(250, 204, 21, 0.4)',
75
+ },
76
+ '.cm-searchMatch.cm-searchMatch-selected': {
77
+ backgroundColor: 'rgba(250, 204, 21, 0.4)',
78
+ },
79
+ '.cm-selectionMatch': {
80
+ backgroundColor: 'rgba(34, 197, 94, 0.1)',
81
+ },
82
+ '.cm-tooltip': {
83
+ backgroundColor: 'var(--bg-elevated, #1e1e2e)',
84
+ border: '1px solid var(--border-default, rgba(255,255,255,0.1))',
85
+ borderRadius: '6px',
86
+ },
87
+ '.cm-panels': {
88
+ backgroundColor: 'var(--bg-elevated, #1e1e2e)',
89
+ color: '#e2e8f0',
90
+ },
91
+ '.cm-panels.cm-panels-top': {
92
+ borderBottom: '1px solid var(--border-default, rgba(255,255,255,0.1))',
93
+ },
94
+ '.cm-panel.cm-search': {
95
+ padding: '4px 8px',
96
+ },
97
+ '.cm-panel.cm-search input': {
98
+ background: 'var(--bg-tertiary, #1a1a2e)',
99
+ border: '1px solid var(--border-default)',
100
+ color: '#e2e8f0',
101
+ borderRadius: '4px',
102
+ padding: '2px 6px',
103
+ outline: 'none',
104
+ },
105
+ '.cm-panel.cm-search button': {
106
+ background: 'transparent',
107
+ color: '#e2e8f0',
108
+ border: 'none',
109
+ cursor: 'pointer',
110
+ opacity: '0.7',
111
+ },
112
+ '.cm-panel.cm-search button:hover': {
113
+ opacity: '1',
114
+ },
115
+ '.cm-scroller': {
116
+ overflow: 'auto',
117
+ },
118
+ '.cm-line': {
119
+ padding: '0 4px',
120
+ },
121
+ }, { dark: true });
122
+
123
+ // ─── Syntax highlighting colors ─────────────────────────
124
+ const gitMapsHighlight = HighlightStyle.define([
125
+ { tag: t.keyword, color: '#c084fc' }, // purple for keywords
126
+ { tag: t.controlKeyword, color: '#c084fc' },
127
+ { tag: t.operatorKeyword, color: '#c084fc' },
128
+ { tag: t.definitionKeyword, color: '#c084fc' },
129
+ { tag: t.moduleKeyword, color: '#c084fc' },
130
+ { tag: t.operator, color: '#94a3b8' },
131
+ { tag: t.comment, color: '#6b7280', fontStyle: 'italic' },
132
+ { tag: t.lineComment, color: '#6b7280', fontStyle: 'italic' },
133
+ { tag: t.blockComment, color: '#6b7280', fontStyle: 'italic' },
134
+ { tag: t.docComment, color: '#6b7280', fontStyle: 'italic' },
135
+ { tag: t.string, color: '#34d399' }, // green for strings
136
+ { tag: t.special(t.string), color: '#34d399' },
137
+ { tag: t.number, color: '#f59e0b' }, // amber for numbers
138
+ { tag: t.integer, color: '#f59e0b' },
139
+ { tag: t.float, color: '#f59e0b' },
140
+ { tag: t.bool, color: '#f59e0b' },
141
+ { tag: t.null, color: '#f59e0b' },
142
+ { tag: t.regexp, color: '#f97316' }, // orange for regex
143
+ { tag: t.variableName, color: '#e2e8f0' },
144
+ { tag: t.definition(t.variableName), color: '#60a5fa' }, // blue for definitions
145
+ { tag: t.function(t.variableName), color: '#60a5fa' }, // blue for function names
146
+ { tag: t.typeName, color: '#38bdf8' }, // sky blue for types
147
+ { tag: t.className, color: '#38bdf8' },
148
+ { tag: t.namespace, color: '#38bdf8' },
149
+ { tag: t.propertyName, color: '#93c5fd' }, // light blue for properties
150
+ { tag: t.definition(t.propertyName), color: '#93c5fd' },
151
+ { tag: t.function(t.propertyName), color: '#60a5fa' },
152
+ { tag: t.macroName, color: '#f97316' },
153
+ { tag: t.labelName, color: '#c084fc' },
154
+ { tag: t.meta, color: '#94a3b8' },
155
+ { tag: t.annotation, color: '#f59e0b' },
156
+ { tag: t.modifier, color: '#c084fc' },
157
+ { tag: t.self, color: '#c084fc' },
158
+ { tag: t.processingInstruction, color: '#94a3b8' },
159
+ { tag: t.attributeName, color: '#93c5fd' }, // HTML/JSX attributes
160
+ { tag: t.attributeValue, color: '#34d399' },
161
+ { tag: t.angleBracket, color: '#94a3b8' },
162
+ { tag: t.tagName, color: '#f472b6' }, // pink for HTML tags
163
+ { tag: t.heading, color: '#60a5fa', fontWeight: 'bold' },
164
+ { tag: t.emphasis, fontStyle: 'italic' },
165
+ { tag: t.strong, fontWeight: 'bold' },
166
+ { tag: t.link, color: '#60a5fa', textDecoration: 'underline' },
167
+ { tag: t.url, color: '#60a5fa', textDecoration: 'underline' },
168
+ { tag: t.inserted, color: '#34d399' },
169
+ { tag: t.deleted, color: '#ef4444' },
170
+ { tag: t.changed, color: '#f59e0b' },
171
+ { tag: t.invalid, color: '#ef4444' },
172
+ { tag: t.escape, color: '#f97316' },
173
+ { tag: t.punctuation, color: '#94a3b8' },
174
+ { tag: t.bracket, color: '#94a3b8' },
175
+ { tag: t.separator, color: '#94a3b8' },
176
+ { tag: t.paren, color: '#94a3b8' },
177
+ { tag: t.squareBracket, color: '#94a3b8' },
178
+ { tag: t.brace, color: '#94a3b8' },
179
+ { tag: t.derefOperator, color: '#94a3b8' },
180
+ ]);
181
+
182
+ // ─── Language detection ─────────────────────────────────
183
+ function getLanguageExtension(ext: string) {
184
+ switch (ext) {
185
+ case 'js':
186
+ case 'mjs':
187
+ case 'cjs':
188
+ return javascript();
189
+ case 'jsx':
190
+ return javascript({ jsx: true });
191
+ case 'ts':
192
+ case 'mts':
193
+ case 'cts':
194
+ return javascript({ typescript: true });
195
+ case 'tsx':
196
+ return javascript({ typescript: true, jsx: true });
197
+ case 'css':
198
+ case 'scss':
199
+ case 'less':
200
+ return css();
201
+ case 'html':
202
+ case 'htm':
203
+ case 'svg':
204
+ return html();
205
+ case 'json':
206
+ case 'jsonc':
207
+ return json();
208
+ case 'md':
209
+ case 'mdx':
210
+ return markdown();
211
+ case 'py':
212
+ case 'pyw':
213
+ return python();
214
+ case 'yaml':
215
+ case 'yml':
216
+ return yaml();
217
+ default:
218
+ return null;
219
+ }
220
+ }
221
+
222
+ // ─── Create Editor ──────────────────────────────────────
223
+ export interface EditorInstance {
224
+ view: EditorView;
225
+ getContent: () => string;
226
+ setContent: (content: string) => void;
227
+ getCursorPosition: () => { line: number; col: number };
228
+ scrollToLine: (line: number) => void;
229
+ destroy: () => void;
230
+ focus: () => void;
231
+ }
232
+
233
+ export function createCodeEditor(
234
+ parent: HTMLElement,
235
+ content: string,
236
+ ext: string,
237
+ options: {
238
+ onSave?: () => void;
239
+ onChange?: (content: string) => void;
240
+ onCursorMove?: (line: number, col: number) => void;
241
+ } = {}
242
+ ): EditorInstance {
243
+ const langCompartment = new Compartment();
244
+ const langExt = getLanguageExtension(ext);
245
+
246
+ const extensions = [
247
+ lineNumbers(),
248
+ highlightActiveLineGutter(),
249
+ highlightSpecialChars(),
250
+ history(),
251
+ foldGutter(),
252
+ drawSelection(),
253
+ rectangularSelection(),
254
+ indentOnInput(),
255
+ bracketMatching(),
256
+ closeBrackets(),
257
+ highlightActiveLine(),
258
+ highlightSelectionMatches(),
259
+ syntaxHighlighting(gitMapsHighlight),
260
+ gitMapsDarkTheme,
261
+ keymap.of([
262
+ ...closeBracketsKeymap,
263
+ ...defaultKeymap,
264
+ ...searchKeymap,
265
+ ...historyKeymap,
266
+ ...foldKeymap,
267
+ indentWithTab,
268
+ ]),
269
+ // Ctrl+S save handler
270
+ keymap.of([{
271
+ key: 'Mod-s',
272
+ run: () => {
273
+ options.onSave?.();
274
+ return true;
275
+ },
276
+ }]),
277
+ // Cursor position tracking
278
+ EditorView.updateListener.of((update) => {
279
+ if (update.selectionSet || update.docChanged) {
280
+ const pos = update.state.selection.main.head;
281
+ const line = update.state.doc.lineAt(pos);
282
+ options.onCursorMove?.(line.number, pos - line.from + 1);
283
+ }
284
+ if (update.docChanged) {
285
+ options.onChange?.(update.state.doc.toString());
286
+ }
287
+ }),
288
+ // Prevent escape from bubbling (we handle it in the modal)
289
+ EditorView.domEventHandlers({
290
+ keydown(e) {
291
+ if (e.key === 'Escape') {
292
+ // Let the modal handle Escape
293
+ return false;
294
+ }
295
+ },
296
+ }),
297
+ EditorView.lineWrapping,
298
+ minimapExtension(),
299
+ ];
300
+
301
+ if (langExt) {
302
+ extensions.push(langCompartment.of(langExt));
303
+ }
304
+
305
+ const state = EditorState.create({
306
+ doc: content,
307
+ extensions,
308
+ });
309
+
310
+ const view = new EditorView({
311
+ state,
312
+ parent,
313
+ });
314
+
315
+ return {
316
+ view,
317
+ getContent: () => view.state.doc.toString(),
318
+ setContent: (newContent: string) => {
319
+ view.dispatch({
320
+ changes: {
321
+ from: 0,
322
+ to: view.state.doc.length,
323
+ insert: newContent,
324
+ },
325
+ });
326
+ },
327
+ getCursorPosition: () => {
328
+ const pos = view.state.selection.main.head;
329
+ const line = view.state.doc.lineAt(pos);
330
+ return { line: line.number, col: pos - line.from + 1 };
331
+ },
332
+ scrollToLine: (lineNum: number) => {
333
+ const line = view.state.doc.line(Math.min(lineNum, view.state.doc.lines));
334
+ view.dispatch({
335
+ selection: { anchor: line.from },
336
+ scrollIntoView: true,
337
+ });
338
+ },
339
+ destroy: () => view.destroy(),
340
+ focus: () => view.focus(),
341
+ };
342
+ }
343
+
344
+ // ─── Minimap Extension ──────────────────────────────────
345
+
346
+ function minimapExtension() {
347
+ return ViewPlugin.fromClass(class {
348
+ wrapper: HTMLElement;
349
+ canvas: HTMLCanvasElement;
350
+ viewport: HTMLElement;
351
+ ctx: CanvasRenderingContext2D;
352
+ isDragging = false;
353
+ rafId = 0;
354
+
355
+ constructor(public view: EditorView) {
356
+ // Wrapper
357
+ this.wrapper = document.createElement('div');
358
+ this.wrapper.className = 'cm-minimap';
359
+ this.wrapper.style.cssText = `
360
+ position: absolute; top: 0; right: 0; width: 60px; height: 100%;
361
+ background: rgba(10, 10, 18, 0.85); border-left: 1px solid rgba(255,255,255,0.06);
362
+ z-index: 5; cursor: pointer; overflow: hidden;
363
+ `;
364
+
365
+ // Canvas for rendering minimap text
366
+ this.canvas = document.createElement('canvas');
367
+ this.canvas.style.cssText = 'width: 100%; height: 100%; display: block;';
368
+ this.wrapper.appendChild(this.canvas);
369
+ this.ctx = this.canvas.getContext('2d')!;
370
+
371
+ // Viewport indicator
372
+ this.viewport = document.createElement('div');
373
+ this.viewport.className = 'cm-minimap-viewport';
374
+ this.viewport.style.cssText = `
375
+ position: absolute; left: 0; right: 0;
376
+ background: rgba(139, 92, 246, 0.12);
377
+ border: 1px solid rgba(139, 92, 246, 0.3);
378
+ border-radius: 2px; pointer-events: none;
379
+ transition: top 0.05s ease-out;
380
+ `;
381
+ this.wrapper.appendChild(this.viewport);
382
+
383
+ // Mount into the editor's scroll parent
384
+ const scroller = view.scrollDOM;
385
+ scroller.style.position = 'relative';
386
+ scroller.appendChild(this.wrapper);
387
+
388
+ // Click to scroll
389
+ this.wrapper.addEventListener('mousedown', this.onMouseDown);
390
+
391
+ // Initial render
392
+ requestAnimationFrame(() => { this.render(); this.updateViewport(); });
393
+ }
394
+
395
+ update(update: ViewUpdate) {
396
+ if (update.docChanged || update.viewportChanged || update.geometryChanged) {
397
+ this.scheduleRender();
398
+ }
399
+ }
400
+
401
+ scheduleRender() {
402
+ if (this.rafId) return;
403
+ this.rafId = requestAnimationFrame(() => {
404
+ this.rafId = 0;
405
+ this.render();
406
+ this.updateViewport();
407
+ });
408
+ }
409
+
410
+ render() {
411
+ const { canvas, ctx, view } = this;
412
+ const dpr = window.devicePixelRatio || 1;
413
+ const rect = this.wrapper.getBoundingClientRect();
414
+ const w = rect.width;
415
+ const h = rect.height;
416
+
417
+ canvas.width = w * dpr;
418
+ canvas.height = h * dpr;
419
+ ctx.scale(dpr, dpr);
420
+ ctx.clearRect(0, 0, w, h);
421
+
422
+ const doc = view.state.doc;
423
+ const totalLines = doc.lines;
424
+ if (totalLines === 0) return;
425
+
426
+ const lineH = Math.max(1, Math.min(2, h / totalLines));
427
+ const colors: Record<string, string> = {
428
+ keyword: '#c084fc', string: '#34d399', comment: '#4b5563',
429
+ number: '#f59e0b', type: '#38bdf8', default: '#64748b',
430
+ };
431
+
432
+ for (let i = 1; i <= totalLines; i++) {
433
+ const line = doc.line(i);
434
+ const text = line.text;
435
+ if (!text.trim()) continue;
436
+
437
+ const y = (i - 1) * lineH;
438
+ if (y > h) break;
439
+
440
+ // Determine color based on simple heuristics
441
+ const trimmed = text.trimStart();
442
+ let color = colors.default;
443
+ if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) color = colors.comment;
444
+ else if (/^(import|export|const|let|var|function|class|if|for|while|return|async|await)\b/.test(trimmed)) color = colors.keyword;
445
+ else if (/['"`]/.test(trimmed)) color = colors.string;
446
+ else if (/\d/.test(trimmed.charAt(0))) color = colors.number;
447
+
448
+ ctx.fillStyle = color;
449
+ // Draw a thin line representing the code
450
+ const indent = (text.length - trimmed.length) * 0.5;
451
+ const lineLen = Math.min(trimmed.length * 0.4, w - 4);
452
+ ctx.globalAlpha = 0.6;
453
+ ctx.fillRect(2 + indent, y, lineLen, Math.max(lineH - 0.5, 0.5));
454
+ }
455
+ ctx.globalAlpha = 1;
456
+ }
457
+
458
+ updateViewport() {
459
+ const { view, wrapper, viewport } = this;
460
+ const scrollEl = view.scrollDOM;
461
+ const totalH = scrollEl.scrollHeight;
462
+ const visibleH = scrollEl.clientHeight;
463
+ const scrollTop = scrollEl.scrollTop;
464
+ const mapH = wrapper.getBoundingClientRect().height;
465
+
466
+ if (totalH <= visibleH) {
467
+ viewport.style.top = '0px';
468
+ viewport.style.height = `${mapH}px`;
469
+ return;
470
+ }
471
+
472
+ const ratio = mapH / totalH;
473
+ viewport.style.top = `${scrollTop * ratio}px`;
474
+ viewport.style.height = `${Math.max(8, visibleH * ratio)}px`;
475
+ }
476
+
477
+ onMouseDown = (e: MouseEvent) => {
478
+ if (e.target === this.viewport) return;
479
+ e.preventDefault();
480
+ this.isDragging = true;
481
+ this.scrollToY(e.clientY);
482
+
483
+ const onMove = (e: MouseEvent) => {
484
+ if (this.isDragging) this.scrollToY(e.clientY);
485
+ };
486
+ const onUp = () => {
487
+ this.isDragging = false;
488
+ document.removeEventListener('mousemove', onMove);
489
+ document.removeEventListener('mouseup', onUp);
490
+ };
491
+ document.addEventListener('mousemove', onMove);
492
+ document.addEventListener('mouseup', onUp);
493
+ };
494
+
495
+ scrollToY(clientY: number) {
496
+ const rect = this.wrapper.getBoundingClientRect();
497
+ const fraction = (clientY - rect.top) / rect.height;
498
+ const scrollEl = this.view.scrollDOM;
499
+ scrollEl.scrollTop = fraction * (scrollEl.scrollHeight - scrollEl.clientHeight);
500
+ this.updateViewport();
501
+ }
502
+
503
+ destroy() {
504
+ if (this.rafId) cancelAnimationFrame(this.rafId);
505
+ this.wrapper.remove();
506
+ }
507
+ });
508
+ }