-
-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathpath.ts
More file actions
155 lines (145 loc) · 5.54 KB
/
Copy pathpath.ts
File metadata and controls
155 lines (145 loc) · 5.54 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
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2025 Lulu (GitHub: lulu-sk, https://github.com/lulu-sk)
import path from "node:path";
import { isUNCPath, uncToWsl } from "../../wsl";
/**
* 中文说明:去掉路径尾部分隔符,但保留“根目录”语义。
* - `C:` / `C:\` / `C:/` 统一保留为 `C:\`;
* - `/mnt/c/` 保留为 `/mnt/c`;
* - `/` 保留为 `/`;
* - 其余路径去掉多余的尾部分隔符。
*/
function trimTrailingSeparatorsPreserveRoot(value: string): string {
try {
const raw = String(value || "").trim();
if (!raw) return "";
const normalized = raw.replace(/\//g, "\\");
const driveRootMatch = normalized.match(/^([a-zA-Z]):(?:\\+)?$/);
if (driveRootMatch) return `${driveRootMatch[1].toUpperCase()}:\\`;
const posixLike = raw.replace(/\\/g, "/");
const mountRootMatch = posixLike.match(/^\/mnt\/([a-zA-Z])\/?$/);
if (mountRootMatch) return `/mnt/${mountRootMatch[1].toLowerCase()}`;
if (/^\/+$/.test(posixLike)) return "/";
return raw.replace(/[\\/]+$/g, "");
} catch {
return String(value || "").trim();
}
}
/**
* 中文说明:将 dirKey/scope 统一规范化为可比较的 key。
* - `C:` / `C:\foo` 转为 `/mnt/c` 风格;
* - 其余路径统一为小写 POSIX 风格。
*/
function normalizeDirKeyScopeValue(value: string): string {
const raw = String(value || "").trim();
if (!raw) return "";
const driveMatch = raw.match(/^([a-zA-Z]):(?:[\\/](.*))?$/);
if (driveMatch) {
const drive = driveMatch[1].toLowerCase();
const rest = String(driveMatch[2] || "").replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "");
return rest ? `/mnt/${drive}/${rest}` : `/mnt/${drive}`;
}
const normalized = raw.replace(/\\/g, "/").replace(/\/+/g, "/");
if (normalized === "/") return "/";
return normalized.replace(/\/+$/, "").toLowerCase();
}
/**
* 清理从日志/JSON 中提取的路径候选:
* - 去除首尾空白与包裹引号
* - 折叠 JSON 转义反斜杠(例如 C:\\code -> C:\code)
* - 去除尾部分隔符
*/
export function tidyPathCandidate(value: string): string {
try {
let s = String(value || "")
.replace(/\\n/g, "")
.replace(/^"|"$/g, "")
.replace(/^'|'$/g, "")
.trim();
s = s.replace(/\\\\/g, "\\").trim();
s = trimTrailingSeparatorsPreserveRoot(s);
return s;
} catch {
return String(value || "").trim();
}
}
/**
* 中文说明:判断某个规范化后的目录 scope 是否应只允许“精确匹配”。
* - 盘符根目录(如 `/mnt/c`)不应吞掉整盘所有子目录会话;
* - POSIX 根目录(`/`)同样仅允许匹配自身。
*/
export function isExactMatchOnlyDirKeyScope(scopeKey: string): boolean {
const scope = normalizeDirKeyScopeValue(scopeKey);
if (!scope) return false;
if (scope === "/") return true;
if (/^\/mnt\/[a-z]$/.test(scope)) return true;
return false;
}
/**
* 中文说明:判断候选 dirKey 是否属于指定 scope。
* - 普通项目目录:允许“自身或子目录”命中;
* - 根目录 scope:仅允许精确命中,避免 `C:\` 吞掉 `C:\Users\...`。
*/
export function pathMatchesDirKeyScope(candidateKey: string, scopeKey: string): boolean {
const candidate = normalizeDirKeyScopeValue(candidateKey);
const scope = normalizeDirKeyScopeValue(scopeKey);
if (!candidate || !scope) return false;
if (candidate === scope) return true;
if (isExactMatchOnlyDirKeyScope(scope)) return false;
return candidate.startsWith(`${scope}/`);
}
/**
* 中文说明:在多个项目 scope 中,为候选路径选择“最具体”的命中项。
* - 无命中时返回空串;
* - 父子项目同时命中时,优先返回路径更长的子项目 scope。
*/
export function findBestMatchingDirKeyScope(candidateKey: string, scopeKeys: readonly string[]): string {
const candidate = normalizeDirKeyScopeValue(candidateKey);
if (!candidate) return "";
let best = "";
for (const rawScope of scopeKeys) {
const scope = normalizeDirKeyScopeValue(rawScope);
if (!scope || !pathMatchesDirKeyScope(candidate, scope)) continue;
if (!best || scope.length > best.length) best = scope;
}
return best;
}
/**
* 从文件路径获取用于项目归属匹配的 dirKey(优先归一为 WSL 风格)。
*/
export function dirKeyOfFilePath(filePath: string): string {
try {
const d = path.dirname(filePath);
const s = d.replace(/\\/g, "/").replace(/\/+/g, "/");
const m = s.match(/^([a-zA-Z]):\/(.*)$/);
if (m) return (`/mnt/${m[1].toLowerCase()}/${m[2]}`).replace(/\/+/g, "/").replace(/\/+$/, "").toLowerCase();
if (isUNCPath(d)) {
const info = uncToWsl(d);
if (info) return info.wslPath.replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\/+$/, "").toLowerCase();
}
return s.replace(/\/+$/, "").toLowerCase();
} catch {
return String(filePath || "").replace(/\\/g, "/").toLowerCase();
}
}
/**
* 从 cwd/项目路径计算用于匹配的 dirKey(不降一级目录)。
*/
export function dirKeyFromCwd(dirPath: string): string {
try {
let d = tidyPathCandidate(dirPath);
if (isUNCPath(d)) {
const info = uncToWsl(d);
if (info) d = info.wslPath;
} else {
const m = d.match(/^([a-zA-Z]):(?:\\(.*))?$/);
if (m) {
const rest = String(m[2] || "").replace(/\\/g, "/");
d = rest ? `/mnt/${m[1].toLowerCase()}/${rest}` : `/mnt/${m[1].toLowerCase()}`;
}
}
return d.replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\/+$/, "").toLowerCase();
} catch {
return String(dirPath || "").replace(/\\/g, "/").toLowerCase();
}
}