@youtyan/code-viewer 0.1.15 → 0.1.17
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 +34 -8
- package/dist/code-viewer.js +2235 -0
- package/package.json +23 -18
- package/web/app.js +514 -5
- package/web/style.css +44 -3
- package/web-src/routes.ts +0 -148
- package/web-src/server/cache.ts +0 -64
- package/web-src/server/dev-assets.ts +0 -37
- package/web-src/server/dev.ts +0 -100
- package/web-src/server/git.ts +0 -483
- package/web-src/server/preview.ts +0 -985
- package/web-src/server/range.ts +0 -8
- package/web-src/server/runtime.d.ts +0 -51
- package/web-src/server/search.ts +0 -104
- package/web-src/types.ts +0 -136
package/web/style.css
CHANGED
|
@@ -2350,12 +2350,45 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
|
|
|
2350
2350
|
text-overflow: ellipsis;
|
|
2351
2351
|
white-space: nowrap;
|
|
2352
2352
|
}
|
|
2353
|
-
.gdp-source-virtual-
|
|
2353
|
+
.gdp-source-virtual-search {
|
|
2354
2354
|
display: inline-flex;
|
|
2355
2355
|
align-items: center;
|
|
2356
2356
|
gap: 6px;
|
|
2357
|
+
min-width: 0;
|
|
2357
2358
|
margin-left: auto;
|
|
2358
2359
|
}
|
|
2360
|
+
.gdp-source-virtual-search[hidden] {
|
|
2361
|
+
display: none;
|
|
2362
|
+
}
|
|
2363
|
+
.gdp-source-virtual-search input {
|
|
2364
|
+
width: min(220px, 28vw);
|
|
2365
|
+
min-width: 120px;
|
|
2366
|
+
height: 28px;
|
|
2367
|
+
padding: 0 8px;
|
|
2368
|
+
border: 1px solid var(--border);
|
|
2369
|
+
border-radius: 6px;
|
|
2370
|
+
background: var(--bg);
|
|
2371
|
+
color: var(--fg);
|
|
2372
|
+
}
|
|
2373
|
+
.gdp-source-virtual-search button {
|
|
2374
|
+
height: 28px;
|
|
2375
|
+
padding: 0 8px;
|
|
2376
|
+
border: 1px solid var(--border);
|
|
2377
|
+
border-radius: 6px;
|
|
2378
|
+
background: var(--bg-subtle);
|
|
2379
|
+
color: var(--fg);
|
|
2380
|
+
cursor: pointer;
|
|
2381
|
+
}
|
|
2382
|
+
.gdp-source-virtual-search-count {
|
|
2383
|
+
min-width: 62px;
|
|
2384
|
+
color: var(--fg-muted);
|
|
2385
|
+
font-size: 12px;
|
|
2386
|
+
}
|
|
2387
|
+
.gdp-source-virtual-actions {
|
|
2388
|
+
display: inline-flex;
|
|
2389
|
+
align-items: center;
|
|
2390
|
+
gap: 6px;
|
|
2391
|
+
}
|
|
2359
2392
|
.gdp-source-virtual-action {
|
|
2360
2393
|
display: inline-flex;
|
|
2361
2394
|
align-items: center;
|
|
@@ -2385,8 +2418,7 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
|
|
|
2385
2418
|
font-family: "Monaspace Neon", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
|
2386
2419
|
font-size: var(--code-font-size);
|
|
2387
2420
|
line-height: 20px;
|
|
2388
|
-
cursor:
|
|
2389
|
-
user-select: none;
|
|
2421
|
+
cursor: text;
|
|
2390
2422
|
}
|
|
2391
2423
|
.gdp-source-virtual-spacer {
|
|
2392
2424
|
position: relative;
|
|
@@ -2426,6 +2458,15 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
|
|
|
2426
2458
|
.gdp-source-virtual-line-code.hljs {
|
|
2427
2459
|
background: var(--bg);
|
|
2428
2460
|
}
|
|
2461
|
+
.gdp-source-virtual-search-hit {
|
|
2462
|
+
background: #fff8c5;
|
|
2463
|
+
color: inherit;
|
|
2464
|
+
border-radius: 2px;
|
|
2465
|
+
}
|
|
2466
|
+
.gdp-source-virtual-search-hit.active {
|
|
2467
|
+
background: var(--accent);
|
|
2468
|
+
color: var(--bg);
|
|
2469
|
+
}
|
|
2429
2470
|
.gdp-standalone-source.gdp-file-shell {
|
|
2430
2471
|
border: 0;
|
|
2431
2472
|
border-radius: 0;
|
package/web-src/routes.ts
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
export type DiffRange = {
|
|
2
|
-
from: string;
|
|
3
|
-
to: string;
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
export type SourceLineRange = {
|
|
7
|
-
start: number;
|
|
8
|
-
end: number;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export type SourceLineTarget = number | SourceLineRange;
|
|
12
|
-
|
|
13
|
-
export type SourceFileTarget = {
|
|
14
|
-
path: string;
|
|
15
|
-
ref: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type AppRoute =
|
|
19
|
-
| { screen: 'repo'; ref: string; path: string; range: DiffRange }
|
|
20
|
-
| { screen: 'diff'; range: DiffRange; path?: string; line?: SourceLineTarget }
|
|
21
|
-
| { screen: 'file'; path: string; ref: string; range: DiffRange; view?: 'blob' | 'detail'; line?: SourceLineTarget }
|
|
22
|
-
| { screen: 'help'; range: DiffRange; lang: string; section: string }
|
|
23
|
-
| { screen: 'unknown'; reason: 'unknown-pathname' | 'missing-path'; rawPathname: string; rawSearch: string; range: DiffRange };
|
|
24
|
-
|
|
25
|
-
export const SPA_PATHS = ['/todif', '/todiff', '/file', '/help'] as const;
|
|
26
|
-
export const APP_ENTRY_PATHS = ['/', '/index.html'] as const;
|
|
27
|
-
|
|
28
|
-
export function assertNever(value: never): never {
|
|
29
|
-
throw new Error('unhandled route: ' + JSON.stringify(value));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function parseLegacyRange(value: string | null | undefined, fallback: DiffRange): DiffRange {
|
|
33
|
-
const raw = value || '';
|
|
34
|
-
const sep = raw.indexOf('..');
|
|
35
|
-
if (sep < 0) return fallback;
|
|
36
|
-
return {
|
|
37
|
-
from: raw.slice(0, sep) || fallback.from,
|
|
38
|
-
to: raw.slice(sep + 2) || fallback.to,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function parseLineTarget(value: string | null | undefined): SourceLineTarget | undefined {
|
|
43
|
-
const raw = value || '';
|
|
44
|
-
const range = /^(\d+)-(\d+)$/.exec(raw);
|
|
45
|
-
if (range) {
|
|
46
|
-
const a = Number(range[1]);
|
|
47
|
-
const b = Number(range[2]);
|
|
48
|
-
const start = Math.min(a, b);
|
|
49
|
-
const end = Math.max(a, b);
|
|
50
|
-
if (start > 0) return { start, end };
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
const line = Number(raw);
|
|
54
|
-
return Number.isInteger(line) && line > 0 ? line : undefined;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function formatLineTarget(line: SourceLineTarget): string {
|
|
58
|
-
return typeof line === 'number' ? String(line) : line.start + '-' + line.end;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function parseRoute(pathname: string, search: string, fallbackRange: DiffRange): AppRoute {
|
|
62
|
-
const params = new URLSearchParams(search);
|
|
63
|
-
const legacyRange = parseLegacyRange(params.get('range'), fallbackRange);
|
|
64
|
-
const range = {
|
|
65
|
-
from: params.get('from') || legacyRange.from,
|
|
66
|
-
to: params.get('to') || legacyRange.to,
|
|
67
|
-
};
|
|
68
|
-
switch (pathname) {
|
|
69
|
-
case '/':
|
|
70
|
-
case '/index.html':
|
|
71
|
-
return {
|
|
72
|
-
screen: 'repo',
|
|
73
|
-
ref: params.get('ref') || params.get('target') || 'worktree',
|
|
74
|
-
path: params.get('path') || '',
|
|
75
|
-
range,
|
|
76
|
-
};
|
|
77
|
-
case '/todif':
|
|
78
|
-
case '/todiff':
|
|
79
|
-
return {
|
|
80
|
-
screen: 'diff',
|
|
81
|
-
range,
|
|
82
|
-
...(params.get('path') ? { path: params.get('path') || '' } : {}),
|
|
83
|
-
...(parseLineTarget(params.get('line')) ? { line: parseLineTarget(params.get('line')) } : {}),
|
|
84
|
-
};
|
|
85
|
-
case '/file': {
|
|
86
|
-
const path = params.get('path') || '';
|
|
87
|
-
const target = params.get('target') || '';
|
|
88
|
-
const ref = target || params.get('ref') || 'worktree';
|
|
89
|
-
const line = parseLineTarget(params.get('line'));
|
|
90
|
-
if (!path) return { screen: 'unknown', reason: 'missing-path', rawPathname: pathname, rawSearch: search, range };
|
|
91
|
-
return { screen: 'file', path, ref, range, view: target ? 'blob' : 'detail', ...(line ? { line } : {}) };
|
|
92
|
-
}
|
|
93
|
-
case '/help':
|
|
94
|
-
return {
|
|
95
|
-
screen: 'help',
|
|
96
|
-
range,
|
|
97
|
-
lang: params.get('lang') || 'en',
|
|
98
|
-
section: params.get('section') || 'keybindings',
|
|
99
|
-
};
|
|
100
|
-
default:
|
|
101
|
-
return { screen: 'unknown', reason: 'unknown-pathname', rawPathname: pathname, rawSearch: search, range };
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function buildRoute(route: AppRoute): string {
|
|
106
|
-
switch (route.screen) {
|
|
107
|
-
case 'repo': {
|
|
108
|
-
const params = new URLSearchParams();
|
|
109
|
-
if (route.ref && route.ref !== 'worktree') params.set('ref', route.ref);
|
|
110
|
-
if (route.path) params.set('path', route.path);
|
|
111
|
-
const qs = params.toString();
|
|
112
|
-
return '/' + (qs ? '?' + qs : '');
|
|
113
|
-
}
|
|
114
|
-
case 'file':
|
|
115
|
-
if (route.view === 'blob') {
|
|
116
|
-
return '/file?path=' + encodeURIComponent(route.path) +
|
|
117
|
-
'&target=' + encodeURIComponent(route.ref || 'worktree') +
|
|
118
|
-
(route.line ? '&line=' + encodeURIComponent(formatLineTarget(route.line)) : '');
|
|
119
|
-
}
|
|
120
|
-
return '/file?path=' + encodeURIComponent(route.path) +
|
|
121
|
-
'&ref=' + encodeURIComponent(route.ref || 'worktree') +
|
|
122
|
-
'&from=' + encodeURIComponent(route.range.from || '') +
|
|
123
|
-
'&to=' + encodeURIComponent(route.range.to || 'worktree') +
|
|
124
|
-
(route.line ? '&line=' + encodeURIComponent(formatLineTarget(route.line)) : '');
|
|
125
|
-
case 'diff':
|
|
126
|
-
return '/todif?from=' + encodeURIComponent(route.range.from || '') +
|
|
127
|
-
'&to=' + encodeURIComponent(route.range.to || 'worktree') +
|
|
128
|
-
(route.path ? '&path=' + encodeURIComponent(route.path) : '') +
|
|
129
|
-
(route.line ? '&line=' + encodeURIComponent(formatLineTarget(route.line)) : '');
|
|
130
|
-
case 'help': {
|
|
131
|
-
const params = new URLSearchParams();
|
|
132
|
-
if (route.lang && route.lang !== 'en') params.set('lang', route.lang);
|
|
133
|
-
if (route.section && route.section !== 'keybindings') params.set('section', route.section);
|
|
134
|
-
const qs = params.toString();
|
|
135
|
-
return '/help' + (qs ? '?' + qs : '');
|
|
136
|
-
}
|
|
137
|
-
case 'unknown':
|
|
138
|
-
return '/todif?from=' + encodeURIComponent(route.range.from || '') +
|
|
139
|
-
'&to=' + encodeURIComponent(route.range.to || 'worktree');
|
|
140
|
-
default:
|
|
141
|
-
return assertNever(route);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function buildRawFileUrl(target: SourceFileTarget): string {
|
|
146
|
-
return '/_file?path=' + encodeURIComponent(target.path) +
|
|
147
|
-
'&ref=' + encodeURIComponent(target.ref || 'worktree');
|
|
148
|
-
}
|
package/web-src/server/cache.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { lstatSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
|
|
4
|
-
// Short enough that a browser reload self-heals stale git data, while still
|
|
5
|
-
// coalescing bursts from one render pass.
|
|
6
|
-
export const CACHE_TTL_MS = 1500;
|
|
7
|
-
export const MAX_TIMED_CACHE_ENTRIES = 200;
|
|
8
|
-
|
|
9
|
-
export type TimedCacheEntry<T> = T & { storedAt: number };
|
|
10
|
-
|
|
11
|
-
export function cacheFresh<T>(
|
|
12
|
-
cached: TimedCacheEntry<T> | undefined,
|
|
13
|
-
now = Date.now(),
|
|
14
|
-
ttlMs = CACHE_TTL_MS,
|
|
15
|
-
): cached is TimedCacheEntry<T> {
|
|
16
|
-
return !!cached && now - cached.storedAt <= ttlMs;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function setTimedCacheEntry<T>(
|
|
20
|
-
cache: Map<string, TimedCacheEntry<T>>,
|
|
21
|
-
key: string,
|
|
22
|
-
value: T,
|
|
23
|
-
now = Date.now(),
|
|
24
|
-
maxEntries = MAX_TIMED_CACHE_ENTRIES,
|
|
25
|
-
): void {
|
|
26
|
-
cache.set(key, { ...value, storedAt: now });
|
|
27
|
-
while (cache.size > maxEntries) {
|
|
28
|
-
const oldest = cache.keys().next().value;
|
|
29
|
-
if (oldest === undefined) break;
|
|
30
|
-
cache.delete(oldest);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function worktreeFileSignature(path: string, cwd: string): string {
|
|
35
|
-
try {
|
|
36
|
-
const stats = lstatSync(join(cwd, path));
|
|
37
|
-
const inode = 'ino' in stats ? stats.ino : 0;
|
|
38
|
-
return `state:file|size:${stats.size}|mtime:${stats.mtimeMs}|ctime:${stats.ctimeMs}|ino:${inode}`;
|
|
39
|
-
} catch {
|
|
40
|
-
return 'state:missing';
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function fileDiffCacheKey(options: {
|
|
45
|
-
path: string;
|
|
46
|
-
oldPath?: string | null;
|
|
47
|
-
isUntracked: boolean;
|
|
48
|
-
range: { from?: string; to?: string };
|
|
49
|
-
extras: string[];
|
|
50
|
-
args: string[];
|
|
51
|
-
cwd: string;
|
|
52
|
-
}): string {
|
|
53
|
-
const worktreeTarget = options.range.from === 'worktree' || !options.range.to || options.range.to === 'worktree';
|
|
54
|
-
if (options.isUntracked && !worktreeTarget) {
|
|
55
|
-
throw new Error('untracked file diffs require a worktree range');
|
|
56
|
-
}
|
|
57
|
-
const signature = worktreeTarget
|
|
58
|
-
? `\0${worktreeFileSignature(options.path, options.cwd)}`
|
|
59
|
-
: '';
|
|
60
|
-
if (options.isUntracked) {
|
|
61
|
-
return `u\0${options.path}${signature}\0${options.extras.join('\0')}`;
|
|
62
|
-
}
|
|
63
|
-
return `t\0${options.path}\0${options.oldPath || ''}${signature}\0${[...options.extras, ...options.args].join('\0')}`;
|
|
64
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { basename } from 'node:path';
|
|
2
|
-
|
|
3
|
-
export type WatchFn = (
|
|
4
|
-
path: string,
|
|
5
|
-
options: { persistent?: boolean },
|
|
6
|
-
listener: (eventType: string, filename: string | Buffer | null) => void,
|
|
7
|
-
) => unknown;
|
|
8
|
-
|
|
9
|
-
type DevAssetReloadOptions = {
|
|
10
|
-
enabled: boolean;
|
|
11
|
-
webRoot: string;
|
|
12
|
-
watchedFiles: readonly string[];
|
|
13
|
-
watch: WatchFn;
|
|
14
|
-
sendReload: () => void;
|
|
15
|
-
setTimeoutFn?: typeof setTimeout;
|
|
16
|
-
clearTimeoutFn?: typeof clearTimeout;
|
|
17
|
-
debounceMs?: number;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export function startDevAssetReload(options: DevAssetReloadOptions): boolean {
|
|
21
|
-
if (!options.enabled) return false;
|
|
22
|
-
const watched = new Set(options.watchedFiles);
|
|
23
|
-
const setTimer = options.setTimeoutFn || setTimeout;
|
|
24
|
-
const clearTimer = options.clearTimeoutFn || clearTimeout;
|
|
25
|
-
const debounceMs = options.debounceMs ?? 150;
|
|
26
|
-
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
27
|
-
|
|
28
|
-
options.watch(options.webRoot, { persistent: false }, (_event, filename) => {
|
|
29
|
-
if (!filename || !watched.has(basename(filename.toString()))) return;
|
|
30
|
-
if (timer) clearTimer(timer);
|
|
31
|
-
timer = setTimer(() => {
|
|
32
|
-
timer = null;
|
|
33
|
-
options.sendReload();
|
|
34
|
-
}, debounceMs);
|
|
35
|
-
});
|
|
36
|
-
return true;
|
|
37
|
-
}
|
package/web-src/server/dev.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
import { readdirSync, statSync } from 'node:fs';
|
|
4
|
-
import { join, normalize } from 'node:path';
|
|
5
|
-
|
|
6
|
-
const ROOT = normalize(join(import.meta.dir, '..', '..'));
|
|
7
|
-
const SERVER_ROOT = join(ROOT, 'web-src', 'server');
|
|
8
|
-
const DEFAULT_DEV_PORT = 64160;
|
|
9
|
-
|
|
10
|
-
type ChildProcess = {
|
|
11
|
-
kill(signal?: string): void;
|
|
12
|
-
exited: Promise<number>;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
let server: ChildProcess | null = null;
|
|
16
|
-
let build: ChildProcess | null = null;
|
|
17
|
-
let restarting = false;
|
|
18
|
-
let firstStart = true;
|
|
19
|
-
|
|
20
|
-
function withDefaultPort(args: string[]) {
|
|
21
|
-
if (args.includes('--port')) return args;
|
|
22
|
-
return ['--port', String(DEFAULT_DEV_PORT), ...args];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function withoutOpen(args: string[]) {
|
|
26
|
-
return args.filter((arg) => arg !== '--open');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function serverArgs() {
|
|
30
|
-
const args = withDefaultPort(process.argv.slice(2));
|
|
31
|
-
return firstStart ? args : withoutOpen(args);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function watchedFiles() {
|
|
35
|
-
return readdirSync(SERVER_ROOT)
|
|
36
|
-
.filter((name) => name.endsWith('.ts') && name !== 'runtime.d.ts')
|
|
37
|
-
.map((name) => join(SERVER_ROOT, name))
|
|
38
|
-
.concat(join(ROOT, 'web-src', 'types.ts'));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function watchSignature() {
|
|
42
|
-
return watchedFiles()
|
|
43
|
-
.map((file) => `${file}:${statSync(file).mtimeMs}`)
|
|
44
|
-
.join('|');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function startBuild() {
|
|
48
|
-
build = Bun.spawn([
|
|
49
|
-
'bun', 'build', '--watch', '--target=browser', '--format=iife',
|
|
50
|
-
'--outfile=web/app.js', 'web-src/app.ts',
|
|
51
|
-
], { cwd: ROOT, stdout: 'inherit', stderr: 'inherit' }) as ChildProcess;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function startServer() {
|
|
55
|
-
const args = serverArgs();
|
|
56
|
-
firstStart = false;
|
|
57
|
-
server = Bun.spawn([
|
|
58
|
-
'bun', 'run', 'web-src/server/preview.ts', ...args,
|
|
59
|
-
], {
|
|
60
|
-
cwd: ROOT,
|
|
61
|
-
stdout: 'inherit',
|
|
62
|
-
stderr: 'inherit',
|
|
63
|
-
env: { ...process.env, CODE_VIEWER_DEV: '1' },
|
|
64
|
-
}) as ChildProcess;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function restartServer() {
|
|
68
|
-
if (restarting) return;
|
|
69
|
-
restarting = true;
|
|
70
|
-
const old = server;
|
|
71
|
-
server = null;
|
|
72
|
-
if (old) {
|
|
73
|
-
old.kill();
|
|
74
|
-
await old.exited.catch(() => 1);
|
|
75
|
-
}
|
|
76
|
-
startServer();
|
|
77
|
-
restarting = false;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function shutdown() {
|
|
81
|
-
if (server) server.kill();
|
|
82
|
-
if (build) build.kill();
|
|
83
|
-
process.exit(0);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
process.on('SIGINT', shutdown);
|
|
87
|
-
process.on('SIGTERM', shutdown);
|
|
88
|
-
|
|
89
|
-
console.log(`code-viewer dev server watching ${SERVER_ROOT}`);
|
|
90
|
-
startBuild();
|
|
91
|
-
startServer();
|
|
92
|
-
|
|
93
|
-
let sig = watchSignature();
|
|
94
|
-
setInterval(() => {
|
|
95
|
-
const next = watchSignature();
|
|
96
|
-
if (next === sig) return;
|
|
97
|
-
sig = next;
|
|
98
|
-
console.log('server source changed; restarting preview server');
|
|
99
|
-
restartServer();
|
|
100
|
-
}, 500);
|