@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/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-actions {
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: pointer;
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
- }
@@ -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
- }
@@ -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);