-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpaths.ts
More file actions
174 lines (157 loc) · 5.09 KB
/
Copy pathpaths.ts
File metadata and controls
174 lines (157 loc) · 5.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
/**
* Posix path helpers shared by every layer of the bridge.
*
* VS Code hands us `Uri.path` (always posix, always leading slash), Nodepod's
* VFS is posix, and R2 keys are posix-ish — so one small module keeps the three
* from drifting. Normalisation also rejects `..` traversal, which matters on the
* server where a path arrives straight off the wire.
*/
export const WORKSPACE_ROOT = "/workspace";
export class InvalidPathError extends Error {
constructor(path: string) {
super(`invalid path: ${path}`);
this.name = "InvalidPathError";
}
}
/**
* Collapses `.`, rejects `..`, strips duplicate and trailing slashes, and
* guarantees a leading slash. `normalizePath("/a//b/")` -> `"/a/b"`.
*/
export function normalizePath(input: string | undefined | null): string {
const raw = String(input ?? "/");
const segments: string[] = [];
for (const segment of raw.split("/")) {
if (!segment || segment === ".") continue;
if (segment === "..") throw new InvalidPathError(raw);
segments.push(segment);
}
return `/${segments.join("/")}`;
}
export function dirname(path: string): string {
const normalized = normalizePath(path);
if (normalized === "/") return "/";
const index = normalized.lastIndexOf("/");
return index <= 0 ? "/" : normalized.slice(0, index);
}
export function basename(path: string): string {
const normalized = normalizePath(path);
if (normalized === "/") return "";
return normalized.slice(normalized.lastIndexOf("/") + 1);
}
export function extname(path: string): string {
const name = basename(path);
const index = name.lastIndexOf(".");
return index <= 0 ? "" : name.slice(index);
}
export function joinPath(...parts: Array<string | undefined>): string {
return normalizePath(parts.filter(Boolean).join("/"));
}
/** True when `child` is `parent` itself or lives underneath it. */
export function isUnder(parent: string, child: string): boolean {
const p = normalizePath(parent);
const c = normalizePath(child);
if (p === "/") return true;
return c === p || c.startsWith(`${p}/`);
}
/** `relativePath("/workspace", "/workspace/src/a.ts")` -> `"src/a.ts"`. */
export function relativePath(from: string, to: string): string {
const base = normalizePath(from);
const target = normalizePath(to);
if (base === "/") return target.slice(1);
if (target === base) return "";
if (!target.startsWith(`${base}/`)) return target.slice(1);
return target.slice(base.length + 1);
}
/* -------------------------------------------------------------------------- */
/* Glob matching */
/* -------------------------------------------------------------------------- */
const globCache = new Map<string, RegExp>();
/**
* Compiles the glob subset VS Code's search `includes`/`excludes` actually use:
* `**`, `*`, `?`, and `{a,b}` alternation. Anything fancier (extglob, negation)
* is deliberately not supported — the search providers treat an unmatched
* pattern as "no filter" rather than silently dropping results.
*/
export function globToRegExp(glob: string): RegExp {
const cached = globCache.get(glob);
if (cached) return cached;
let out = "";
for (let i = 0; i < glob.length; i++) {
const char = glob[i];
if (char === "*") {
if (glob[i + 1] === "*") {
// `**/` should also match zero directories, so make the slash optional.
if (glob[i + 2] === "/") {
out += "(?:.*/)?";
i += 2;
} else {
out += ".*";
i += 1;
}
} else {
out += "[^/]*";
}
continue;
}
if (char === "?") {
out += "[^/]";
continue;
}
if (char === "{") {
out += "(?:";
continue;
}
if (char === "}") {
out += ")";
continue;
}
if (char === ",") {
out += "|";
continue;
}
out += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
}
const regex = new RegExp(`^${out}$`);
globCache.set(glob, regex);
return regex;
}
export function matchesAnyGlob(
relative: string,
globs: string[] | undefined,
): boolean {
if (!globs || globs.length === 0) return false;
return globs.some((glob) => {
try {
return globToRegExp(glob).test(relative);
} catch {
return false;
}
});
}
/**
* Whether a *directory* is excluded, i.e. whether it is worth descending into.
*
* `**` + `/` + `**` patterns like `**/node_modules/**` describe the contents of
* a directory, not the directory itself, so testing the bare name never matches
* and a tree walk would happily recurse into `node_modules`. Testing the name
* with a trailing slash makes the trailing `**` match the empty remainder.
*/
export function isExcludedDirectory(
relative: string,
globs: string[] | undefined,
): boolean {
return (
matchesAnyGlob(relative, globs) || matchesAnyGlob(`${relative}/`, globs)
);
}
/** Directories that never carry user intent and dominate a naive walk. */
export const DEFAULT_SEARCH_EXCLUDES = [
"**/node_modules/**",
"**/.git/**",
"**/dist/**",
"**/build/**",
"**/.next/**",
"**/out/**",
"**/coverage/**",
"**/.cache/**",
];