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.
- package/README.md +167 -0
- package/app/api/auth/favorites/route.ts +56 -0
- package/app/api/auth/github/callback/route.ts +103 -0
- package/app/api/auth/github/route.ts +32 -0
- package/app/api/auth/me/route.ts +52 -0
- package/app/api/auth/positions/route.ts +50 -0
- package/app/api/chat/route.ts +101 -0
- package/app/api/connections/route.ts +72 -0
- package/app/api/github/repos/route.ts +111 -0
- package/app/api/positions/route.ts +80 -0
- package/app/api/repo/branch-diff/route.ts +201 -0
- package/app/api/repo/branches/route.ts +53 -0
- package/app/api/repo/browse/route.ts +55 -0
- package/app/api/repo/clone/route.ts +78 -0
- package/app/api/repo/clone-stream/route.ts +131 -0
- package/app/api/repo/file-content/route.ts +28 -0
- package/app/api/repo/file-delete/route.ts +62 -0
- package/app/api/repo/file-history/route.ts +45 -0
- package/app/api/repo/file-rename/route.ts +83 -0
- package/app/api/repo/file-save/route.ts +45 -0
- package/app/api/repo/files/route.ts +169 -0
- package/app/api/repo/git-blame/route.ts +86 -0
- package/app/api/repo/git-commit/route.ts +40 -0
- package/app/api/repo/git-heatmap/route.ts +55 -0
- package/app/api/repo/imports/route.ts +154 -0
- package/app/api/repo/load/route.ts +56 -0
- package/app/api/repo/mode/route.ts +14 -0
- package/app/api/repo/search/route.ts +127 -0
- package/app/api/repo/tree/route.ts +104 -0
- package/app/api/repo/upload/route.ts +53 -0
- package/app/api/repo/validate-path.ts +53 -0
- package/app/canvas_users.db +0 -0
- package/app/canvas_users.db-shm +0 -0
- package/app/canvas_users.db-wal +0 -0
- package/app/globals.css +7899 -0
- package/app/layout.tsx +493 -0
- package/app/lib/auth.ts +193 -0
- package/app/lib/auto-save.ts +137 -0
- package/app/lib/branch-compare.ts +443 -0
- package/app/lib/breadcrumbs.ts +170 -0
- package/app/lib/canvas-export.ts +358 -0
- package/app/lib/canvas-text.ts +912 -0
- package/app/lib/canvas.ts +564 -0
- package/app/lib/card-arrangement.ts +188 -0
- package/app/lib/card-context-menu.tsx +453 -0
- package/app/lib/card-diff-markers.ts +270 -0
- package/app/lib/card-expand.ts +189 -0
- package/app/lib/card-groups.ts +246 -0
- package/app/lib/cards.tsx +914 -0
- package/app/lib/chat.tsx +308 -0
- package/app/lib/code-editor.ts +508 -0
- package/app/lib/command-palette.ts +262 -0
- package/app/lib/connections.tsx +1037 -0
- package/app/lib/context.ts +94 -0
- package/app/lib/cursor-sharing.ts +281 -0
- package/app/lib/dependency-graph.ts +438 -0
- package/app/lib/events.tsx +1747 -0
- package/app/lib/file-card-plugin.ts +134 -0
- package/app/lib/file-modal.tsx +849 -0
- package/app/lib/file-preview.ts +400 -0
- package/app/lib/file-tabs.ts +318 -0
- package/app/lib/galaxydraw-bridge.ts +477 -0
- package/app/lib/galaxydraw.test.ts +229 -0
- package/app/lib/global-search.ts +264 -0
- package/app/lib/goto-definition.ts +224 -0
- package/app/lib/heatmap.ts +178 -0
- package/app/lib/hidden-files.tsx +222 -0
- package/app/lib/layers.ts +0 -0
- package/app/lib/layers.tsx +365 -0
- package/app/lib/loading.tsx +45 -0
- package/app/lib/multi-repo.ts +286 -0
- package/app/lib/new-file-dialog.tsx +230 -0
- package/app/lib/onboarding.tsx +213 -0
- package/app/lib/perf-overlay.ts +360 -0
- package/app/lib/positions.ts +176 -0
- package/app/lib/pr-review.ts +374 -0
- package/app/lib/production-mode.ts +47 -0
- package/app/lib/repo.tsx +977 -0
- package/app/lib/settings-modal.tsx +374 -0
- package/app/lib/settings.ts +97 -0
- package/app/lib/shortcuts-panel.ts +141 -0
- package/app/lib/status-bar.ts +128 -0
- package/app/lib/symbol-outline.ts +212 -0
- package/app/lib/syntax.ts +177 -0
- package/app/lib/tab-diff.ts +238 -0
- package/app/lib/user.tsx +133 -0
- package/app/lib/utils.ts +78 -0
- package/app/lib/viewport-culling.ts +728 -0
- package/app/page.client.tsx +215 -0
- package/app/page.tsx +291 -0
- package/app/state/machine.js +196 -0
- package/app/styles/main.css +2168 -0
- package/banner.png +0 -0
- package/cli.ts +44 -0
- package/package.json +75 -0
- package/packages/galaxydraw/README.md +296 -0
- package/packages/galaxydraw/banner.png +0 -0
- package/packages/galaxydraw/demo/build-static.ts +100 -0
- package/packages/galaxydraw/demo/client.ts +154 -0
- package/packages/galaxydraw/demo/dist/client.js +8 -0
- package/packages/galaxydraw/demo/index.html +256 -0
- package/packages/galaxydraw/demo/server.ts +96 -0
- package/packages/galaxydraw/dist/index.js +984 -0
- package/packages/galaxydraw/dist/index.js.map +16 -0
- 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 +49 -0
- package/packages/galaxydraw/perf.test.ts +284 -0
- package/packages/galaxydraw/src/core/cards.ts +435 -0
- package/packages/galaxydraw/src/core/engine.ts +339 -0
- package/packages/galaxydraw/src/core/events.ts +81 -0
- package/packages/galaxydraw/src/core/layout.ts +136 -0
- package/packages/galaxydraw/src/core/minimap.ts +216 -0
- package/packages/galaxydraw/src/core/state.ts +177 -0
- package/packages/galaxydraw/src/core/viewport.ts +106 -0
- package/packages/galaxydraw/src/galaxydraw.css +166 -0
- package/packages/galaxydraw/src/index.ts +40 -0
- package/packages/galaxydraw/tsconfig.json +30 -0
- package/server.ts +62 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Bar — VS Code-style bottom bar for GitMaps
|
|
3
|
+
*
|
|
4
|
+
* Shows: zoom %, file count, selected count, repo name, mode.
|
|
5
|
+
* Updates reactively via exported update functions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CanvasContext } from './context';
|
|
9
|
+
|
|
10
|
+
let bar: HTMLElement | null = null;
|
|
11
|
+
let ctx: CanvasContext | null = null;
|
|
12
|
+
|
|
13
|
+
// Cached state for efficient updates
|
|
14
|
+
let _zoom = 1;
|
|
15
|
+
let _fileCount = 0;
|
|
16
|
+
let _selectedCount = 0;
|
|
17
|
+
let _repoName = '';
|
|
18
|
+
let _mode = 'Simple';
|
|
19
|
+
let _commitHash = '';
|
|
20
|
+
|
|
21
|
+
function createBar(): HTMLElement {
|
|
22
|
+
const el = document.createElement('div');
|
|
23
|
+
el.id = 'status-bar';
|
|
24
|
+
el.innerHTML = `
|
|
25
|
+
<div class="sb-left">
|
|
26
|
+
<span class="sb-item sb-repo" id="sbRepo" title="Current repository"></span>
|
|
27
|
+
<span class="sb-item sb-commit" id="sbCommit" title="Current commit"></span>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="sb-right">
|
|
30
|
+
<span class="sb-item sb-mode" id="sbMode" title="Interaction mode"></span>
|
|
31
|
+
<span class="sb-item sb-selected" id="sbSelected" title="Selected cards"></span>
|
|
32
|
+
<span class="sb-item sb-files" id="sbFiles" title="Total files on canvas"></span>
|
|
33
|
+
<span class="sb-item sb-zoom" id="sbZoom" title="Zoom level (scroll to zoom)"></span>
|
|
34
|
+
</div>
|
|
35
|
+
`;
|
|
36
|
+
return el;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function render() {
|
|
40
|
+
if (!bar) return;
|
|
41
|
+
|
|
42
|
+
const repoEl = bar.querySelector('#sbRepo') as HTMLElement;
|
|
43
|
+
const commitEl = bar.querySelector('#sbCommit') as HTMLElement;
|
|
44
|
+
const modeEl = bar.querySelector('#sbMode') as HTMLElement;
|
|
45
|
+
const selectedEl = bar.querySelector('#sbSelected') as HTMLElement;
|
|
46
|
+
const filesEl = bar.querySelector('#sbFiles') as HTMLElement;
|
|
47
|
+
const zoomEl = bar.querySelector('#sbZoom') as HTMLElement;
|
|
48
|
+
|
|
49
|
+
if (repoEl) repoEl.textContent = _repoName ? `📂 ${_repoName}` : '';
|
|
50
|
+
if (commitEl) commitEl.textContent = _commitHash ? `⊙ ${_commitHash.substring(0, 7)}` : '';
|
|
51
|
+
if (modeEl) {
|
|
52
|
+
modeEl.textContent = `${_mode === 'Advanced' ? '🎯' : '✋'} ${_mode}`;
|
|
53
|
+
modeEl.className = `sb-item sb-mode sb-mode--${_mode.toLowerCase()}`;
|
|
54
|
+
}
|
|
55
|
+
if (selectedEl) {
|
|
56
|
+
selectedEl.textContent = _selectedCount > 0 ? `☑ ${_selectedCount} selected` : '';
|
|
57
|
+
selectedEl.style.display = _selectedCount > 0 ? '' : 'none';
|
|
58
|
+
}
|
|
59
|
+
if (filesEl) filesEl.textContent = `📄 ${_fileCount} files`;
|
|
60
|
+
if (zoomEl) zoomEl.textContent = `🔍 ${Math.round(_zoom * 100)}%`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Public API ──────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
export function initStatusBar(context: CanvasContext) {
|
|
66
|
+
ctx = context;
|
|
67
|
+
bar = createBar();
|
|
68
|
+
|
|
69
|
+
// Insert after canvas-area
|
|
70
|
+
const canvasArea = document.querySelector('.canvas-area');
|
|
71
|
+
if (canvasArea) {
|
|
72
|
+
canvasArea.parentElement?.insertBefore(bar, canvasArea.nextSibling);
|
|
73
|
+
} else {
|
|
74
|
+
document.body.appendChild(bar);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Initial sync
|
|
78
|
+
const state = ctx.snap().context;
|
|
79
|
+
_zoom = state.zoom || 1;
|
|
80
|
+
_repoName = (state.repoPath || '').split('/').pop() || '';
|
|
81
|
+
_fileCount = ctx.fileCards.size;
|
|
82
|
+
_mode = state.mode === 'advanced' ? 'Advanced' : 'Simple';
|
|
83
|
+
_commitHash = state.currentCommitHash || '';
|
|
84
|
+
render();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function updateStatusBarZoom(zoom: number) {
|
|
88
|
+
if (Math.round(zoom * 100) === Math.round(_zoom * 100)) return;
|
|
89
|
+
_zoom = zoom;
|
|
90
|
+
const el = bar?.querySelector('#sbZoom') as HTMLElement;
|
|
91
|
+
if (el) el.textContent = `🔍 ${Math.round(zoom * 100)}%`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function updateStatusBarFiles(count: number) {
|
|
95
|
+
_fileCount = count;
|
|
96
|
+
const el = bar?.querySelector('#sbFiles') as HTMLElement;
|
|
97
|
+
if (el) el.textContent = `📄 ${count} files`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function updateStatusBarSelected(count: number) {
|
|
101
|
+
_selectedCount = count;
|
|
102
|
+
const el = bar?.querySelector('#sbSelected') as HTMLElement;
|
|
103
|
+
if (el) {
|
|
104
|
+
el.textContent = count > 0 ? `☑ ${count} selected` : '';
|
|
105
|
+
el.style.display = count > 0 ? '' : 'none';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function updateStatusBarRepo(repoPath: string) {
|
|
110
|
+
_repoName = repoPath.split('/').pop() || repoPath.split('\\').pop() || '';
|
|
111
|
+
const el = bar?.querySelector('#sbRepo') as HTMLElement;
|
|
112
|
+
if (el) el.textContent = `📂 ${_repoName}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function updateStatusBarCommit(hash: string) {
|
|
116
|
+
_commitHash = hash;
|
|
117
|
+
const el = bar?.querySelector('#sbCommit') as HTMLElement;
|
|
118
|
+
if (el) el.textContent = hash ? `⊙ ${hash.substring(0, 7)}` : '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function updateStatusBarMode(mode: string) {
|
|
122
|
+
_mode = mode;
|
|
123
|
+
const el = bar?.querySelector('#sbMode') as HTMLElement;
|
|
124
|
+
if (el) {
|
|
125
|
+
el.textContent = `${mode === 'Advanced' ? '🎯' : '✋'} ${mode}`;
|
|
126
|
+
el.className = `sb-item sb-mode sb-mode--${mode.toLowerCase()}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Symbol outline panel — extracts functions, classes, interfaces, types,
|
|
4
|
+
* and exports from file content and renders them as a navigable list.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface SymbolEntry {
|
|
8
|
+
name: string;
|
|
9
|
+
kind: 'function' | 'class' | 'interface' | 'type' | 'const' | 'variable' | 'enum' | 'method' | 'export';
|
|
10
|
+
line: number;
|
|
11
|
+
indent: number; // nesting depth
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ─── Symbol extraction patterns ─────────────────────
|
|
15
|
+
const PATTERNS: Array<{ regex: RegExp; kind: SymbolEntry['kind'] }> = [
|
|
16
|
+
// JS/TS: export function / async function / function
|
|
17
|
+
{ regex: /^(\s*)(?:export\s+)?(?:async\s+)?function\s+(\w+)/gm, kind: 'function' },
|
|
18
|
+
// JS/TS: arrow function assigned to const/let/var
|
|
19
|
+
{ regex: /^(\s*)(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>/gm, kind: 'function' },
|
|
20
|
+
// JS/TS: class
|
|
21
|
+
{ regex: /^(\s*)(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/gm, kind: 'class' },
|
|
22
|
+
// TS: interface
|
|
23
|
+
{ regex: /^(\s*)(?:export\s+)?interface\s+(\w+)/gm, kind: 'interface' },
|
|
24
|
+
// TS: type alias
|
|
25
|
+
{ regex: /^(\s*)(?:export\s+)?type\s+(\w+)\s*[=<]/gm, kind: 'type' },
|
|
26
|
+
// TS: enum
|
|
27
|
+
{ regex: /^(\s*)(?:export\s+)?enum\s+(\w+)/gm, kind: 'enum' },
|
|
28
|
+
// JS/TS: exported const (non-arrow)
|
|
29
|
+
{ regex: /^(\s*)export\s+(?:const|let|var)\s+(\w+)\s*[=:]/gm, kind: 'const' },
|
|
30
|
+
// Class methods
|
|
31
|
+
{ regex: /^(\s+)(?:async\s+)?(\w+)\s*\([^)]*\)\s*[:{]/gm, kind: 'method' },
|
|
32
|
+
// Python: def / class
|
|
33
|
+
{ regex: /^(\s*)def\s+(\w+)/gm, kind: 'function' },
|
|
34
|
+
{ regex: /^(\s*)class\s+(\w+)/gm, kind: 'class' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract symbols from file content.
|
|
39
|
+
*/
|
|
40
|
+
export function extractSymbols(content: string, fileName: string): SymbolEntry[] {
|
|
41
|
+
const ext = fileName.split('.').pop()?.toLowerCase() || '';
|
|
42
|
+
const isPython = ext === 'py';
|
|
43
|
+
const isCSS = ext === 'css' || ext === 'scss' || ext === 'less';
|
|
44
|
+
const isJSON = ext === 'json';
|
|
45
|
+
const isMarkdown = ext === 'md';
|
|
46
|
+
|
|
47
|
+
// CSS: extract selectors/rules
|
|
48
|
+
if (isCSS) return extractCSSSymbols(content);
|
|
49
|
+
// JSON: extract top-level keys
|
|
50
|
+
if (isJSON) return extractJSONSymbols(content);
|
|
51
|
+
// Markdown: extract headings
|
|
52
|
+
if (isMarkdown) return extractMarkdownSymbols(content);
|
|
53
|
+
|
|
54
|
+
const symbols: SymbolEntry[] = [];
|
|
55
|
+
const lines = content.split('\n');
|
|
56
|
+
|
|
57
|
+
for (const pattern of PATTERNS) {
|
|
58
|
+
// Skip Python-specific patterns for non-Python, and vice versa
|
|
59
|
+
if (isPython && pattern.kind === 'interface') continue;
|
|
60
|
+
if (isPython && pattern.kind === 'type') continue;
|
|
61
|
+
if (isPython && pattern.kind === 'enum') continue;
|
|
62
|
+
|
|
63
|
+
pattern.regex.lastIndex = 0;
|
|
64
|
+
let match;
|
|
65
|
+
while ((match = pattern.regex.exec(content)) !== null) {
|
|
66
|
+
const indent = (match[1] || '').length;
|
|
67
|
+
const name = match[2];
|
|
68
|
+
if (!name || name === 'if' || name === 'for' || name === 'while' || name === 'switch' || name === 'catch' || name === 'return') continue;
|
|
69
|
+
|
|
70
|
+
// Calculate line number
|
|
71
|
+
const beforeMatch = content.slice(0, match.index);
|
|
72
|
+
const line = beforeMatch.split('\n').length;
|
|
73
|
+
|
|
74
|
+
// Skip methods that are too deeply nested (likely control flow)
|
|
75
|
+
if (pattern.kind === 'method' && indent < 2) continue;
|
|
76
|
+
|
|
77
|
+
symbols.push({ name, kind: pattern.kind, line, indent: Math.floor(indent / 2) });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Deduplicate (same name + line)
|
|
82
|
+
const seen = new Set<string>();
|
|
83
|
+
return symbols
|
|
84
|
+
.filter(s => {
|
|
85
|
+
const key = `${s.name}:${s.line}`;
|
|
86
|
+
if (seen.has(key)) return false;
|
|
87
|
+
seen.add(key);
|
|
88
|
+
return true;
|
|
89
|
+
})
|
|
90
|
+
.sort((a, b) => a.line - b.line);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function extractCSSSymbols(content: string): SymbolEntry[] {
|
|
94
|
+
const symbols: SymbolEntry[] = [];
|
|
95
|
+
const regex = /^([.#@][^\s{]+)\s*\{/gm;
|
|
96
|
+
let match;
|
|
97
|
+
while ((match = regex.exec(content)) !== null) {
|
|
98
|
+
const beforeMatch = content.slice(0, match.index);
|
|
99
|
+
const line = beforeMatch.split('\n').length;
|
|
100
|
+
symbols.push({ name: match[1], kind: 'class', line, indent: 0 });
|
|
101
|
+
}
|
|
102
|
+
return symbols.sort((a, b) => a.line - b.line);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function extractJSONSymbols(content: string): SymbolEntry[] {
|
|
106
|
+
const symbols: SymbolEntry[] = [];
|
|
107
|
+
const regex = /^(\s*)"(\w+)"\s*:/gm;
|
|
108
|
+
let match;
|
|
109
|
+
while ((match = regex.exec(content)) !== null) {
|
|
110
|
+
const indent = (match[1] || '').length;
|
|
111
|
+
if (indent > 4) continue; // Only top-level and one level deep
|
|
112
|
+
const beforeMatch = content.slice(0, match.index);
|
|
113
|
+
const line = beforeMatch.split('\n').length;
|
|
114
|
+
symbols.push({ name: match[2], kind: 'const', line, indent: Math.floor(indent / 2) });
|
|
115
|
+
}
|
|
116
|
+
return symbols.sort((a, b) => a.line - b.line);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function extractMarkdownSymbols(content: string): SymbolEntry[] {
|
|
120
|
+
const symbols: SymbolEntry[] = [];
|
|
121
|
+
const regex = /^(#{1,6})\s+(.+)$/gm;
|
|
122
|
+
let match;
|
|
123
|
+
while ((match = regex.exec(content)) !== null) {
|
|
124
|
+
const level = match[1].length;
|
|
125
|
+
const beforeMatch = content.slice(0, match.index);
|
|
126
|
+
const line = beforeMatch.split('\n').length;
|
|
127
|
+
symbols.push({ name: match[2].trim(), kind: 'export', line, indent: level - 1 });
|
|
128
|
+
}
|
|
129
|
+
return symbols.sort((a, b) => a.line - b.line);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Symbol icons ───────────────────────────────────
|
|
133
|
+
const SYMBOL_ICONS: Record<SymbolEntry['kind'], string> = {
|
|
134
|
+
function: 'ƒ',
|
|
135
|
+
class: '◆',
|
|
136
|
+
interface: '◇',
|
|
137
|
+
type: 'T',
|
|
138
|
+
const: '●',
|
|
139
|
+
variable: '○',
|
|
140
|
+
enum: 'E',
|
|
141
|
+
method: 'μ',
|
|
142
|
+
export: '→',
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const SYMBOL_COLORS: Record<SymbolEntry['kind'], string> = {
|
|
146
|
+
function: '#c4b5fd',
|
|
147
|
+
class: '#86efac',
|
|
148
|
+
interface: '#67e8f9',
|
|
149
|
+
type: '#fde68a',
|
|
150
|
+
const: '#93c5fd',
|
|
151
|
+
variable: '#9ca3af',
|
|
152
|
+
enum: '#f9a8d4',
|
|
153
|
+
method: '#a5b4fc',
|
|
154
|
+
export: '#fdba74',
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Render the symbol outline panel.
|
|
159
|
+
*/
|
|
160
|
+
export function renderSymbolOutline(
|
|
161
|
+
container: HTMLElement,
|
|
162
|
+
content: string,
|
|
163
|
+
fileName: string,
|
|
164
|
+
onSymbolClick: (line: number) => void
|
|
165
|
+
) {
|
|
166
|
+
const symbols = extractSymbols(content, fileName);
|
|
167
|
+
|
|
168
|
+
if (symbols.length === 0) {
|
|
169
|
+
container.innerHTML = '<div class="outline-empty">No symbols found</div>';
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
container.innerHTML = '';
|
|
174
|
+
|
|
175
|
+
// Header
|
|
176
|
+
const header = document.createElement('div');
|
|
177
|
+
header.className = 'outline-header';
|
|
178
|
+
header.innerHTML = `<span class="outline-title">Outline</span><span class="outline-count">${symbols.length}</span>`;
|
|
179
|
+
container.appendChild(header);
|
|
180
|
+
|
|
181
|
+
// Symbol list
|
|
182
|
+
const list = document.createElement('div');
|
|
183
|
+
list.className = 'outline-list';
|
|
184
|
+
|
|
185
|
+
for (const sym of symbols) {
|
|
186
|
+
const item = document.createElement('button');
|
|
187
|
+
item.className = 'outline-item';
|
|
188
|
+
item.style.paddingLeft = `${12 + sym.indent * 14}px`;
|
|
189
|
+
item.title = `${sym.kind}: ${sym.name} (line ${sym.line})`;
|
|
190
|
+
|
|
191
|
+
const icon = document.createElement('span');
|
|
192
|
+
icon.className = 'outline-icon';
|
|
193
|
+
icon.textContent = SYMBOL_ICONS[sym.kind];
|
|
194
|
+
icon.style.color = SYMBOL_COLORS[sym.kind];
|
|
195
|
+
item.appendChild(icon);
|
|
196
|
+
|
|
197
|
+
const name = document.createElement('span');
|
|
198
|
+
name.className = 'outline-name';
|
|
199
|
+
name.textContent = sym.name;
|
|
200
|
+
item.appendChild(name);
|
|
201
|
+
|
|
202
|
+
const lineNo = document.createElement('span');
|
|
203
|
+
lineNo.className = 'outline-lineno';
|
|
204
|
+
lineNo.textContent = `:${sym.line}`;
|
|
205
|
+
item.appendChild(lineNo);
|
|
206
|
+
|
|
207
|
+
item.addEventListener('click', () => onSymbolClick(sym.line));
|
|
208
|
+
list.appendChild(item);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
container.appendChild(list);
|
|
212
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Syntax highlighting & diff HTML builders.
|
|
4
|
+
*/
|
|
5
|
+
import { escapeHtml } from './utils';
|
|
6
|
+
|
|
7
|
+
// ─── Check if position is inside a string literal ────────
|
|
8
|
+
export function isInsideString(line: string, idx: number): boolean {
|
|
9
|
+
let inSingle = false, inDouble = false;
|
|
10
|
+
for (let i = 0; i < idx; i++) {
|
|
11
|
+
if (line[i] === "'" && !inDouble) inSingle = !inSingle;
|
|
12
|
+
if (line[i] === '"' && !inSingle) inDouble = !inDouble;
|
|
13
|
+
}
|
|
14
|
+
return inSingle || inDouble;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ─── Syntax-highlighted HTML from file content ───────────
|
|
18
|
+
export function highlightSyntax(content: string, ext: string): string {
|
|
19
|
+
const lines = content.split('\n');
|
|
20
|
+
const isJSLike = ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs'].includes(ext);
|
|
21
|
+
const isPyLike = ['py', 'pyw'].includes(ext);
|
|
22
|
+
const isCSSLike = ['css', 'scss', 'less', 'sass'].includes(ext);
|
|
23
|
+
const isHTMLLike = ['html', 'htm', 'xml', 'svg', 'jsx', 'tsx'].includes(ext);
|
|
24
|
+
const isRustGo = ['rs', 'go'].includes(ext);
|
|
25
|
+
const isJSON = ext === 'json';
|
|
26
|
+
const isMD = ['md', 'mdx'].includes(ext);
|
|
27
|
+
const isYAML = ['yml', 'yaml', 'toml'].includes(ext);
|
|
28
|
+
const isShell = ['sh', 'bash', 'zsh', 'fish', 'bat', 'cmd', 'ps1'].includes(ext);
|
|
29
|
+
const isSQL = ext === 'sql';
|
|
30
|
+
|
|
31
|
+
return lines.map((line, i) => {
|
|
32
|
+
const lineNum = `<span class="line-num">${String(i + 1).padStart(4, ' ')}</span>`;
|
|
33
|
+
let highlighted = escapeHtml(line);
|
|
34
|
+
|
|
35
|
+
if (isJSON) {
|
|
36
|
+
highlighted = highlighted
|
|
37
|
+
.replace(/("[^&]*?")\s*:/g, '<span style="color:#7dd3fc">$1</span>:')
|
|
38
|
+
.replace(/:(\s*)("[^&]*?")/g, ':$1<span style="color:#86efac">$2</span>')
|
|
39
|
+
.replace(/\b(\d+(?:\.\d+)?)\b/g, '<span style="color:#fbbf24">$1</span>')
|
|
40
|
+
.replace(/\b(true|false|null)\b/g, '<span style="color:#c084fc">$1</span>');
|
|
41
|
+
} else if (isCSSLike) {
|
|
42
|
+
highlighted = highlighted
|
|
43
|
+
.replace(/(\/\*.*?\*\/)/g, '<span style="color:#6b7280;font-style:italic">$1</span>')
|
|
44
|
+
.replace(/(\/\/.*$)/g, '<span style="color:#6b7280;font-style:italic">$1</span>')
|
|
45
|
+
.replace(/([.#][\w-]+)/g, '<span style="color:#7dd3fc">$1</span>')
|
|
46
|
+
.replace(/(:\s*)([^;{}]+)(;)/g, '$1<span style="color:#86efac">$2</span>$3')
|
|
47
|
+
.replace(/\b(\d+(?:\.\d+)?(?:px|em|rem|%|vh|vw|s|ms)?)\b/g, '<span style="color:#fbbf24">$1</span>');
|
|
48
|
+
} else if (isMD) {
|
|
49
|
+
highlighted = highlighted
|
|
50
|
+
.replace(/^(#{1,6}\s.*)$/, '<span style="color:#c084fc;font-weight:600">$1</span>')
|
|
51
|
+
.replace(/(\*\*[^*]+\*\*)/g, '<span style="color:#f0abfc;font-weight:600">$1</span>')
|
|
52
|
+
.replace(/(\*[^*]+\*)/g, '<span style="color:#f0abfc;font-style:italic">$1</span>')
|
|
53
|
+
.replace(/(`[^`]+`)/g, '<span style="color:#86efac;background:rgba(134,239,172,0.08);padding:1px 3px;border-radius:2px">$1</span>')
|
|
54
|
+
.replace(/(\[[^\]]+\]\([^)]+\))/g, '<span style="color:#7dd3fc;text-decoration:underline">$1</span>')
|
|
55
|
+
.replace(/^(\s*[-*+]\s)/g, '<span style="color:#fbbf24">$1</span>')
|
|
56
|
+
.replace(/^(\s*\d+\.\s)/g, '<span style="color:#fbbf24">$1</span>')
|
|
57
|
+
.replace(/^(>\s.*)/g, '<span style="color:#6b7280;font-style:italic;border-left:2px solid #6b7280;padding-left:8px">$1</span>');
|
|
58
|
+
} else if (isYAML) {
|
|
59
|
+
highlighted = highlighted
|
|
60
|
+
.replace(/(#.*$)/g, '<span style="color:#6b7280;font-style:italic">$1</span>')
|
|
61
|
+
.replace(/^(\s*[\w.-]+)(\s*[:=])/g, '<span style="color:#7dd3fc">$1</span>$2')
|
|
62
|
+
.replace(/:\s*("[^&]*?")/g, ': <span style="color:#86efac">$1</span>')
|
|
63
|
+
.replace(/:\s*(&#x27;[^&]*?&#x27;)/g, ': <span style="color:#86efac">$1</span>')
|
|
64
|
+
.replace(/\b(true|false|yes|no|null|~)\b/gi, '<span style="color:#c084fc">$1</span>')
|
|
65
|
+
.replace(/\b(\d+(?:\.\d+)?)\b/g, '<span style="color:#fbbf24">$1</span>')
|
|
66
|
+
.replace(/^(\s*-\s)/g, '<span style="color:#fbbf24">$1</span>')
|
|
67
|
+
.replace(/([\w.-]+\])/g, '<span style="color:#c084fc;font-weight:500">$1</span>');
|
|
68
|
+
} else if (isShell) {
|
|
69
|
+
highlighted = highlighted
|
|
70
|
+
.replace(/(#.*$)/g, '<span style="color:#6b7280;font-style:italic">$1</span>')
|
|
71
|
+
.replace(/(\$[\w{][^}\s]*}?)/g, '<span style="color:#7dd3fc">$1</span>')
|
|
72
|
+
.replace(/("[^&]*?")/g, '<span style="color:#86efac">$1</span>')
|
|
73
|
+
.replace(/(&#x27;[^&]*?&#x27;)/g, '<span style="color:#86efac">$1</span>');
|
|
74
|
+
const shKeywords = 'if|then|else|elif|fi|for|while|do|done|case|esac|function|return|exit|echo|export|source|local|set|unset|cd|mkdir|rm|cp|mv|cat|grep|sed|awk|chmod|chown';
|
|
75
|
+
highlighted = highlighted.replace(
|
|
76
|
+
new RegExp(`\\b(${shKeywords})\\b`, 'g'),
|
|
77
|
+
'<span style="color:#c084fc">$1</span>'
|
|
78
|
+
);
|
|
79
|
+
} else if (isSQL) {
|
|
80
|
+
const sqlKeywords = 'SELECT|FROM|WHERE|INSERT|INTO|UPDATE|SET|DELETE|CREATE|DROP|ALTER|TABLE|INDEX|VIEW|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AND|OR|NOT|IN|EXISTS|BETWEEN|LIKE|ORDER|BY|GROUP|HAVING|LIMIT|OFFSET|UNION|ALL|AS|IS|NULL|PRIMARY|KEY|FOREIGN|REFERENCES|UNIQUE|DEFAULT|CHECK|CONSTRAINT|VALUES|COUNT|SUM|AVG|MIN|MAX|DISTINCT';
|
|
81
|
+
highlighted = highlighted
|
|
82
|
+
.replace(/(--.*$)/g, '<span style="color:#6b7280;font-style:italic">$1</span>')
|
|
83
|
+
.replace(/(&#x27;[^&]*?&#x27;)/g, '<span style="color:#86efac">$1</span>');
|
|
84
|
+
highlighted = highlighted.replace(
|
|
85
|
+
new RegExp(`\\b(${sqlKeywords})\\b`, 'gi'),
|
|
86
|
+
'<span style="color:#c084fc">$1</span>'
|
|
87
|
+
);
|
|
88
|
+
highlighted = highlighted.replace(/\b(\d+(?:\.\d+)?)\b/g, '<span style="color:#fbbf24">$1</span>');
|
|
89
|
+
} else if (isJSLike || isPyLike || isRustGo) {
|
|
90
|
+
const commentIdx = highlighted.indexOf('//');
|
|
91
|
+
const hashIdx = isPyLike ? highlighted.indexOf('#') : -1;
|
|
92
|
+
if (commentIdx >= 0 && !isInsideString(line, line.indexOf('//'))) {
|
|
93
|
+
const before = highlighted.substring(0, commentIdx);
|
|
94
|
+
const comment = highlighted.substring(commentIdx);
|
|
95
|
+
highlighted = before + `<span style="color:#6b7280;font-style:italic">${comment}</span>`;
|
|
96
|
+
} else if (hashIdx >= 0 && isPyLike && !isInsideString(line, line.indexOf('#'))) {
|
|
97
|
+
const before = highlighted.substring(0, hashIdx);
|
|
98
|
+
const comment = highlighted.substring(hashIdx);
|
|
99
|
+
highlighted = before + `<span style="color:#6b7280;font-style:italic">${comment}</span>`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
highlighted = highlighted
|
|
103
|
+
.replace(/("(?:[^&]|&(?!quot;))*?")/g, '<span style="color:#86efac">$1</span>')
|
|
104
|
+
.replace(/(&#x27;(?:[^&]|&(?!#x27;))*?&#x27;)/g, '<span style="color:#86efac">$1</span>')
|
|
105
|
+
.replace(/(`[^`]*?`)/g, '<span style="color:#86efac">$1</span>');
|
|
106
|
+
|
|
107
|
+
if (isJSLike || isPyLike) {
|
|
108
|
+
highlighted = highlighted.replace(
|
|
109
|
+
/(@[\w.]+)/g,
|
|
110
|
+
'<span style="color:#fb923c">$1</span>'
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const keywords = isJSLike
|
|
115
|
+
? 'const|let|var|function|return|if|else|for|while|do|switch|case|break|continue|try|catch|finally|throw|new|delete|typeof|instanceof|in|of|class|extends|super|import|export|from|default|async|await|yield|this|null|undefined|true|false|void|static|get|set|type|interface|enum|as|is'
|
|
116
|
+
: isPyLike
|
|
117
|
+
? 'def|class|if|elif|else|for|while|return|import|from|as|try|except|finally|raise|with|yield|lambda|pass|break|continue|and|or|not|in|is|True|False|None|self|async|await'
|
|
118
|
+
: 'fn|let|mut|const|if|else|for|while|loop|match|return|struct|enum|impl|trait|use|mod|pub|self|Self|true|false|async|await|type|func|go|defer|chan|select|range|package|import|var';
|
|
119
|
+
highlighted = highlighted.replace(
|
|
120
|
+
new RegExp(`\\b(${keywords})\\b`, 'g'),
|
|
121
|
+
'<span style="color:#c084fc">$1</span>'
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
highlighted = highlighted.replace(/\b(\d+(?:\.\d+)?(?:e[+-]?\d+)?)\b/gi, '<span style="color:#fbbf24">$1</span>');
|
|
125
|
+
highlighted = highlighted.replace(/\b([A-Z][a-zA-Z0-9_]*)\b/g, '<span style="color:#7dd3fc">$1</span>');
|
|
126
|
+
} else if (isHTMLLike) {
|
|
127
|
+
highlighted = highlighted
|
|
128
|
+
.replace(/(<\/?[\w-]+)/g, '<span style="color:#c084fc">$1</span>')
|
|
129
|
+
.replace(/([\w-]+)(=)/g, '<span style="color:#7dd3fc">$1</span>$2')
|
|
130
|
+
.replace(/("[^&]*?")/g, '<span style="color:#86efac">$1</span>');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return `<span class="diff-line">${lineNum}${highlighted}</span>`;
|
|
134
|
+
}).join('\n');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── Build diff HTML for the modal view ──────────────────
|
|
138
|
+
export function buildModalDiffHTML(file: any): string {
|
|
139
|
+
let html = '';
|
|
140
|
+
|
|
141
|
+
if (file.status === 'added' && file.content) {
|
|
142
|
+
const lines = file.content.split('\n');
|
|
143
|
+
html = lines.map((line: string, i: number) =>
|
|
144
|
+
`<span class="diff-line diff-add" data-line="${i + 1}"><span class="line-num">${String(i + 1).padStart(4, ' ')}</span>+ ${escapeHtml(line)}</span>`
|
|
145
|
+
).join('\n');
|
|
146
|
+
} else if (file.status === 'deleted' && file.content) {
|
|
147
|
+
const lines = file.content.split('\n');
|
|
148
|
+
html = lines.map((line: string, i: number) =>
|
|
149
|
+
`<span class="diff-line diff-del" data-line="${i + 1}"><span class="line-num">${String(i + 1).padStart(4, ' ')}</span>- ${escapeHtml(line)}</span>`
|
|
150
|
+
).join('\n');
|
|
151
|
+
} else if (file.status === 'modified' && file.hunks?.length > 0) {
|
|
152
|
+
const hunkSections = file.hunks.map((hunk: any) => {
|
|
153
|
+
const header = `@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@${hunk.context ? ' ' + escapeHtml(hunk.context) : ''}`;
|
|
154
|
+
let oldLine = hunk.oldStart;
|
|
155
|
+
let newLine = hunk.newStart;
|
|
156
|
+
|
|
157
|
+
const lineItems = hunk.lines.map((l: any) => {
|
|
158
|
+
if (l.type === 'add') {
|
|
159
|
+
const ln = newLine++;
|
|
160
|
+
return `<span class="diff-line diff-add" data-line="${ln}"><span class="line-num">${String(ln).padStart(4, ' ')}</span>+ ${escapeHtml(l.content)}</span>`;
|
|
161
|
+
} else if (l.type === 'del') {
|
|
162
|
+
const ln = oldLine++;
|
|
163
|
+
return `<span class="diff-line diff-del" data-line="${ln}"><span class="line-num">${String(ln).padStart(4, ' ')}</span>- ${escapeHtml(l.content)}</span>`;
|
|
164
|
+
} else {
|
|
165
|
+
oldLine++; newLine++;
|
|
166
|
+
const ln = newLine - 1;
|
|
167
|
+
return `<span class="diff-line diff-ctx" data-line="${ln}"><span class="line-num">${String(ln).padStart(4, ' ')}</span> ${escapeHtml(l.content)}</span>`;
|
|
168
|
+
}
|
|
169
|
+
}).join('\n');
|
|
170
|
+
|
|
171
|
+
return `<span class="diff-line diff-hunk-header-line" style="color:var(--accent-tertiary);background:rgba(124,58,237,0.08);font-weight:500;">${header}</span>\n${lineItems}`;
|
|
172
|
+
});
|
|
173
|
+
html = hunkSections.join('\n\n');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return html || '<span style="color: var(--text-muted); font-style: italic;">No diff available</span>';
|
|
177
|
+
}
|