@youtyan/code-viewer 0.1.10 → 0.1.12
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/package.json +1 -1
- package/web/app.js +826 -29
- package/web/shiki.js +2 -1
- package/web/style.css +192 -0
- package/web-src/routes.ts +43 -7
- package/web-src/server/cache.ts +51 -0
- package/web-src/server/preview.ts +119 -9
- package/web-src/server/search.ts +101 -0
- package/web-src/types.ts +24 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { FileSearchListResponse, GrepMatch } from '../types';
|
|
2
|
+
import type { GitTreeEntry } from './git';
|
|
3
|
+
|
|
4
|
+
export const GREP_DEFAULT_MAX = 200;
|
|
5
|
+
export const GREP_ABSOLUTE_MAX = 500;
|
|
6
|
+
export const GREP_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
7
|
+
export const FILE_SEARCH_ABSOLUTE_MAX = 50000;
|
|
8
|
+
|
|
9
|
+
export function normalizeGrepMax(value: string | null): number {
|
|
10
|
+
const parsed = Number(value || '');
|
|
11
|
+
if (!Number.isInteger(parsed) || parsed <= 0) return GREP_DEFAULT_MAX;
|
|
12
|
+
return Math.min(parsed, GREP_ABSOLUTE_MAX);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isSkippableSearchPath(path: string): boolean {
|
|
16
|
+
return path.split(/[\\/]+/).some(part => {
|
|
17
|
+
const lower = part.toLowerCase();
|
|
18
|
+
return lower === '.git' || lower === 'node_modules';
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function fixedStringLineMatches(path: string, text: string, query: string, max: number): GrepMatch[] {
|
|
23
|
+
const needle = query.toLowerCase();
|
|
24
|
+
if (!needle) return [];
|
|
25
|
+
const matches: GrepMatch[] = [];
|
|
26
|
+
const lines = text.split('\n');
|
|
27
|
+
for (let i = 0; i < lines.length && matches.length < max; i++) {
|
|
28
|
+
const line = lines[i];
|
|
29
|
+
const column = line.toLowerCase().indexOf(needle);
|
|
30
|
+
if (column < 0) continue;
|
|
31
|
+
matches.push({
|
|
32
|
+
path,
|
|
33
|
+
line: i + 1,
|
|
34
|
+
column: column + 1,
|
|
35
|
+
preview: line.slice(0, 500),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return matches;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildFileSearchList(ref: string, generation: number, entries: GitTreeEntry[]): FileSearchListResponse {
|
|
42
|
+
const files = entries
|
|
43
|
+
.filter((entry): entry is GitTreeEntry & { type: 'blob' | 'commit' } => entry.type === 'blob' || entry.type === 'commit')
|
|
44
|
+
.slice(0, FILE_SEARCH_ABSOLUTE_MAX)
|
|
45
|
+
.map(entry => ({ path: entry.path, type: entry.type }));
|
|
46
|
+
return {
|
|
47
|
+
ref,
|
|
48
|
+
generation,
|
|
49
|
+
files,
|
|
50
|
+
truncated: entries.length > FILE_SEARCH_ABSOLUTE_MAX,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildRgArgs(query: string, max: number, paths: string[], regex = false): string[] {
|
|
55
|
+
const safePaths = paths.length ? paths : ['.'];
|
|
56
|
+
const args = [
|
|
57
|
+
'rg',
|
|
58
|
+
'--no-config',
|
|
59
|
+
'--line-number',
|
|
60
|
+
'--column',
|
|
61
|
+
'--no-heading',
|
|
62
|
+
'--color',
|
|
63
|
+
'never',
|
|
64
|
+
'--smart-case',
|
|
65
|
+
'--max-count',
|
|
66
|
+
String(max),
|
|
67
|
+
'--max-filesize',
|
|
68
|
+
'2M',
|
|
69
|
+
'-e',
|
|
70
|
+
query,
|
|
71
|
+
'--',
|
|
72
|
+
...safePaths,
|
|
73
|
+
];
|
|
74
|
+
if (!regex) args.splice(8, 0, '--fixed-strings');
|
|
75
|
+
return args;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parseRgOutput(stdout: string, max: number): GrepMatch[] {
|
|
79
|
+
const matches: GrepMatch[] = [];
|
|
80
|
+
for (const line of stdout.split('\n')) {
|
|
81
|
+
if (!line || matches.length >= max) continue;
|
|
82
|
+
const parsed = /^(.*):(\d+):(\d+):(.*)$/.exec(line);
|
|
83
|
+
if (!parsed) continue;
|
|
84
|
+
const path = parsed[1];
|
|
85
|
+
const lineNo = Number(parsed[2]);
|
|
86
|
+
const column = Number(parsed[3]);
|
|
87
|
+
const preview = parsed[4];
|
|
88
|
+
if (!path || !lineNo || !column || isSkippableSearchPath(path)) continue;
|
|
89
|
+
matches.push({ path, line: lineNo, column, preview: preview.slice(0, 500) });
|
|
90
|
+
}
|
|
91
|
+
return matches;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function parseGitGrepOutput(stdout: string, ref: string, max: number): GrepMatch[] {
|
|
95
|
+
const prefix = ref + ':';
|
|
96
|
+
const normalized = stdout
|
|
97
|
+
.split('\n')
|
|
98
|
+
.map(line => line.startsWith(prefix) ? line.slice(prefix.length) : line)
|
|
99
|
+
.join('\n');
|
|
100
|
+
return parseRgOutput(normalized, max);
|
|
101
|
+
}
|
package/web-src/types.ts
CHANGED
|
@@ -53,6 +53,30 @@ export type RepoTreeResponse = {
|
|
|
53
53
|
} | null;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
export type FileSearchListResponse = {
|
|
57
|
+
ref: string;
|
|
58
|
+
generation: number;
|
|
59
|
+
files: {
|
|
60
|
+
path: string;
|
|
61
|
+
type: 'blob' | 'commit';
|
|
62
|
+
}[];
|
|
63
|
+
truncated: boolean;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type GrepMatch = {
|
|
67
|
+
path: string;
|
|
68
|
+
line: number;
|
|
69
|
+
column: number;
|
|
70
|
+
preview: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type GrepResponse = {
|
|
74
|
+
ref: string;
|
|
75
|
+
engine: 'rg' | 'git' | 'fallback';
|
|
76
|
+
truncated: boolean;
|
|
77
|
+
matches: GrepMatch[];
|
|
78
|
+
};
|
|
79
|
+
|
|
56
80
|
export type FileDiffResponse = {
|
|
57
81
|
path: string;
|
|
58
82
|
old_path?: string;
|