dsh-vscode-mode 0.22.0 → 0.23.0
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 +5 -3
- package/lib/client.js +3370 -85
- package/lib/client.js.map +2 -2
- package/lib/index.js +1167 -266
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +166 -0
- package/src/client/quickMenuHost.ts +46 -0
- package/src/client/quickMenuStore.ts +260 -0
- package/src/client/state/diffSummary.ts +9 -4
- package/src/client/styles/quickmenu.css +1049 -0
- package/src/client/ui/QuickMenuDialog.tsx +835 -0
- package/src/client/ui/QuickMenuLauncher.tsx +792 -0
- package/src/client/ui/QuickMenuPickers.ts +577 -0
- package/src/index.ts +2 -2
- package/src/paths.ts +10 -0
- package/src/pending/capture.ts +104 -75
- package/src/pending/persist.ts +6 -1
- package/src/pending/store.ts +10 -5
- package/src/pending/vcs.ts +2 -2
- package/src/pendingCompat.ts +3 -8
- package/src/pendingRpc.ts +140 -58
- package/src/quickMenu.ts +269 -0
- package/src/rpc.ts +37 -0
- package/src/shared/pathKey.ts +54 -0
- package/src/shared/quickMenu.ts +1009 -0
- package/src/shared/quickPick.ts +67 -0
- package/src/shared/rpc.ts +10 -0
- package/src/shared/skillCatalog.ts +60 -0
- package/src/skillCatalog.ts +138 -0
package/src/quickMenu.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — 快捷菜单配置的读写(~/.dsh/dsh-vscode-mode/data/quick-menu.json)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独成模块:菜单项是用户数据而不是界面镜像——不能落在 cache/(会被 sweep、
|
|
5
|
+
* 且与 uiState 共享 512KB 预算),也不适合塞进 Config schema(嵌套 JSON 不参与校验)。
|
|
6
|
+
* 写入约定:revision 乐观并发 + 同文件串行队列 + tmp 原子替换 + 单代 .bak 备份,
|
|
7
|
+
* 解析失败时把原文件另存 .bad-<ts>.json 再回落默认文档,绝不让坏文件阻塞功能。
|
|
8
|
+
* 作者 ddj 2026年10月30号
|
|
9
|
+
*/
|
|
10
|
+
import { copyFile, mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises'
|
|
11
|
+
import { dirname } from 'node:path'
|
|
12
|
+
import {
|
|
13
|
+
defaultDoc,
|
|
14
|
+
normalizeDoc,
|
|
15
|
+
parseDoc,
|
|
16
|
+
serializeDoc,
|
|
17
|
+
QUICK_MENU_BYTES_CAP,
|
|
18
|
+
QUICK_MENU_MAX_ITEMS,
|
|
19
|
+
} from './shared/quickMenu.js'
|
|
20
|
+
import type { QuickMenuDoc } from './shared/quickMenu.js'
|
|
21
|
+
import { quickMenuFile } from './paths.js'
|
|
22
|
+
|
|
23
|
+
/** 一次性读取结果。 */
|
|
24
|
+
export interface QuickMenuState {
|
|
25
|
+
/** 合法文档(读到坏文件时为默认文档)。 */
|
|
26
|
+
doc: QuickMenuDoc
|
|
27
|
+
/** 读取告警(坏文件被备份并重置时给出)。 */
|
|
28
|
+
warning?: string
|
|
29
|
+
/** 文件最后修改时间毫秒(缺失为 0)。 */
|
|
30
|
+
mtime: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 写入结果(冲突时把磁盘上的最新文档带回,供客户端刷新)。 */
|
|
34
|
+
export type QuickMenuSaveResult =
|
|
35
|
+
| { ok: true; doc: QuickMenuDoc }
|
|
36
|
+
| { ok: false; error: string; doc?: QuickMenuDoc }
|
|
37
|
+
|
|
38
|
+
/** mtime 缓存(文件未变则复用解析结果,避免每次打开菜单都读盘)。 */
|
|
39
|
+
const stateCache = new Map<string, QuickMenuState>()
|
|
40
|
+
|
|
41
|
+
/** 同文件写队列尾(串行化并发写入;队列清空即移除条目,与 uiState 同套路)。 */
|
|
42
|
+
const writeChains = new Map<string, Promise<unknown>>()
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 把写任务挂到该文件的队尾串行执行(前序失败不影响后续)。
|
|
46
|
+
* @author ddj 2026年10月30号
|
|
47
|
+
* @param file 目标文件绝对路径
|
|
48
|
+
* @param run 实际写操作
|
|
49
|
+
* @returns 本次写操作结果
|
|
50
|
+
*/
|
|
51
|
+
function chainWrite<T>(file: string, run: () => Promise<T>): Promise<T> {
|
|
52
|
+
const prev = writeChains.get(file) ?? Promise.resolve()
|
|
53
|
+
const next = prev.then(run, run)
|
|
54
|
+
const tail = next.then(() => undefined, () => undefined)
|
|
55
|
+
writeChains.set(file, tail)
|
|
56
|
+
void tail.then(() => {
|
|
57
|
+
if (writeChains.get(file) === tail) writeChains.delete(file)
|
|
58
|
+
})
|
|
59
|
+
return next
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 读取文件修改时间(缺失/不可读返回 0)。
|
|
64
|
+
* @author ddj 2026年10月30号
|
|
65
|
+
* @param file 文件路径
|
|
66
|
+
* @returns mtime 毫秒
|
|
67
|
+
*/
|
|
68
|
+
async function mtimeOf(file: string): Promise<number> {
|
|
69
|
+
try {
|
|
70
|
+
return (await stat(file)).mtimeMs
|
|
71
|
+
} catch {
|
|
72
|
+
return 0
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 原子写文本(先写 .tmp 再改名,缺目录自动建;失败时尽量清掉临时文件)。
|
|
78
|
+
* @author ddj 2026年10月30号
|
|
79
|
+
* @param file 目标文件
|
|
80
|
+
* @param text 文本
|
|
81
|
+
*/
|
|
82
|
+
async function writeAtomic(file: string, text: string): Promise<void> {
|
|
83
|
+
await mkdir(dirname(file), { recursive: true })
|
|
84
|
+
const temporary = file + '.tmp'
|
|
85
|
+
await writeFile(temporary, text, 'utf8')
|
|
86
|
+
await rename(temporary, file)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 把坏文件另存为 .bad-<ts>.json(尽力而为,失败不影响主流程)。
|
|
91
|
+
* @author ddj 2026年10月30号
|
|
92
|
+
* @param file 源文件
|
|
93
|
+
* @returns 备份路径(失败为 null)
|
|
94
|
+
*/
|
|
95
|
+
async function backupCorrupt(file: string): Promise<string | null> {
|
|
96
|
+
const target = file + '.bad-' + Date.now() + '.json'
|
|
97
|
+
try {
|
|
98
|
+
await copyFile(file, target)
|
|
99
|
+
return target
|
|
100
|
+
} catch {
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 把上一次内容写成单代备份(quick-menu.bak.json)。
|
|
107
|
+
* @author ddj 2026年10月30号
|
|
108
|
+
* @param file 源文件
|
|
109
|
+
*/
|
|
110
|
+
async function backupPrevious(file: string): Promise<void> {
|
|
111
|
+
try {
|
|
112
|
+
await copyFile(file, file.replace(/\.json$/, '') + '.bak.json')
|
|
113
|
+
} catch {
|
|
114
|
+
// 首次保存没有旧文件,忽略
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 读盘并归一(绕过缓存;文件缺失按默认文档处理)。
|
|
120
|
+
* @author ddj 2026年10月30号
|
|
121
|
+
* @param file 文件路径
|
|
122
|
+
* @returns 文档 + 告警 + mtime
|
|
123
|
+
*/
|
|
124
|
+
async function loadFile(file: string): Promise<QuickMenuState> {
|
|
125
|
+
const mtime = await mtimeOf(file)
|
|
126
|
+
if (mtime === 0) return { doc: defaultDoc(), mtime: 0 }
|
|
127
|
+
let text = ''
|
|
128
|
+
try {
|
|
129
|
+
text = await readFile(file, 'utf8')
|
|
130
|
+
} catch (error) {
|
|
131
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
132
|
+
return { doc: defaultDoc(), warning: '快捷菜单配置读取失败(' + reason + '),已回落默认配置', mtime }
|
|
133
|
+
}
|
|
134
|
+
const parsed = parseDoc(text)
|
|
135
|
+
if (!parsed.warning) return { doc: parsed.doc, mtime }
|
|
136
|
+
// 坏文件:先备份原文件,再写回默认文档,避免每次打开都重复告警
|
|
137
|
+
const backup = await backupCorrupt(file)
|
|
138
|
+
const doc = parsed.doc
|
|
139
|
+
try {
|
|
140
|
+
await writeAtomic(file, serializeDoc(doc))
|
|
141
|
+
} catch {
|
|
142
|
+
// 只读环境/权限不足时保留告警即可,功能继续用内存默认文档
|
|
143
|
+
}
|
|
144
|
+
const hint = backup ? '已备份为 ' + backup : '备份失败'
|
|
145
|
+
return { doc, warning: parsed.warning + '(' + hint + ')', mtime }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 读取快捷菜单文档(mtime 未变时复用缓存)。
|
|
150
|
+
* @author ddj 2026年10月30号
|
|
151
|
+
* @param home DSH home(测试可注入)
|
|
152
|
+
* @returns 文档 + 告警
|
|
153
|
+
*/
|
|
154
|
+
export async function readQuickMenu(home?: string): Promise<QuickMenuState> {
|
|
155
|
+
const file = quickMenuFile(home)
|
|
156
|
+
const mtime = await mtimeOf(file)
|
|
157
|
+
const cached = stateCache.get(file)
|
|
158
|
+
if (cached && cached.mtime === mtime) return cached
|
|
159
|
+
const state = await loadFile(file)
|
|
160
|
+
stateCache.set(file, state)
|
|
161
|
+
return state
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 清空读取缓存(测试或外部改动后强制重读)。
|
|
166
|
+
* @author ddj 2026年10月30号
|
|
167
|
+
* @param home DSH home(缺省清全部)
|
|
168
|
+
*/
|
|
169
|
+
export function resetMenuCache(home?: string): void {
|
|
170
|
+
if (home === undefined) stateCache.clear()
|
|
171
|
+
else stateCache.delete(quickMenuFile(home))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 校验待保存文档的硬性上限(超出即拒绝,避免静默截断用户数据)。
|
|
176
|
+
* @author ddj 2026年10月30号
|
|
177
|
+
* @param doc 待保存文档
|
|
178
|
+
* @returns 错误文案(合法为 null)
|
|
179
|
+
*/
|
|
180
|
+
function checkLimits(doc: QuickMenuDoc): string | null {
|
|
181
|
+
if (doc.items.length > QUICK_MENU_MAX_ITEMS) return '菜单项超过上限(' + QUICK_MENU_MAX_ITEMS + ' 项)'
|
|
182
|
+
const bytes = Buffer.byteLength(serializeDoc(doc), 'utf8')
|
|
183
|
+
if (bytes > QUICK_MENU_BYTES_CAP) return '配置超出容量上限(' + QUICK_MENU_BYTES_CAP + ' 字节)'
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 保存文档(revision 不匹配即判定为并发冲突,返回磁盘最新文档)。
|
|
189
|
+
* @author ddj 2026年10月30号
|
|
190
|
+
* @param doc 客户端提交的完整文档
|
|
191
|
+
* @param baseRevision 客户端读到的 revision
|
|
192
|
+
* @param home DSH home(测试可注入)
|
|
193
|
+
* @returns 保存结果
|
|
194
|
+
*/
|
|
195
|
+
export async function saveQuickMenu(
|
|
196
|
+
doc: QuickMenuDoc,
|
|
197
|
+
baseRevision: number,
|
|
198
|
+
home?: string,
|
|
199
|
+
): Promise<QuickMenuSaveResult> {
|
|
200
|
+
const file = quickMenuFile(home)
|
|
201
|
+
return chainWrite(file, async () => {
|
|
202
|
+
const limit = checkLimits(doc)
|
|
203
|
+
if (limit) return { ok: false as const, error: limit }
|
|
204
|
+
const current = await loadFile(file)
|
|
205
|
+
if (current.doc.revision !== baseRevision) {
|
|
206
|
+
return { ok: false as const, error: '配置已被其他窗口修改,请重新加载后再保存', doc: current.doc }
|
|
207
|
+
}
|
|
208
|
+
const next = normalizeDoc(doc)
|
|
209
|
+
// 悬浮按钮坐标只由 patchLauncher 维护:保存表单时沿用磁盘现值,
|
|
210
|
+
// 否则「拖动/重置位置」与「保存设置」并发时会互相覆盖(曾实测重置后被旧坐标顶回)。
|
|
211
|
+
next.launcher = current.doc.launcher
|
|
212
|
+
next.revision = current.doc.revision + 1
|
|
213
|
+
next.updatedAt = new Date().toISOString()
|
|
214
|
+
try {
|
|
215
|
+
await backupPrevious(file)
|
|
216
|
+
await writeAtomic(file, serializeDoc(next))
|
|
217
|
+
} catch (error) {
|
|
218
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
219
|
+
return { ok: false as const, error: '快捷菜单配置写入失败:' + reason }
|
|
220
|
+
}
|
|
221
|
+
const state: QuickMenuState = { doc: next, mtime: await mtimeOf(file) }
|
|
222
|
+
stateCache.set(file, state)
|
|
223
|
+
return { ok: true as const, doc: next }
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 只更新悬浮按钮坐标(不推进 revision,避免拖拽把正在编辑的表单顶掉)。
|
|
229
|
+
* @author ddj 2026年10月30号
|
|
230
|
+
* @param launcher 坐标(null 表示回到输入框锚点)
|
|
231
|
+
* @param home DSH home(测试可注入)
|
|
232
|
+
* @returns 保存结果
|
|
233
|
+
*/
|
|
234
|
+
export async function patchLauncher(
|
|
235
|
+
launcher: { x: number | null; y: number | null },
|
|
236
|
+
home?: string,
|
|
237
|
+
): Promise<QuickMenuSaveResult> {
|
|
238
|
+
const file = quickMenuFile(home)
|
|
239
|
+
return chainWrite(file, async () => {
|
|
240
|
+
const current = await loadFile(file)
|
|
241
|
+
const next: QuickMenuDoc = {
|
|
242
|
+
...current.doc,
|
|
243
|
+
launcher: { x: launcher.x, y: launcher.y },
|
|
244
|
+
updatedAt: new Date().toISOString(),
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
await writeAtomic(file, serializeDoc(next))
|
|
248
|
+
} catch (error) {
|
|
249
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
250
|
+
return { ok: false as const, error: '保存悬浮按钮位置失败:' + reason }
|
|
251
|
+
}
|
|
252
|
+
const state: QuickMenuState = { doc: next, mtime: await mtimeOf(file) }
|
|
253
|
+
stateCache.set(file, state)
|
|
254
|
+
return { ok: true as const, doc: next }
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** 导出/导入时的建议文件名(浏览器侧下载用,host 不参与)。 */
|
|
259
|
+
export const quickMenuBaseName = 'quick-menu.json'
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 当前 home 下的配置路径(诊断日志用)。
|
|
263
|
+
* @author ddj 2026年10月30号
|
|
264
|
+
* @param home DSH home
|
|
265
|
+
* @returns 绝对路径
|
|
266
|
+
*/
|
|
267
|
+
export function quickMenuPath(home?: string): string {
|
|
268
|
+
return quickMenuFile(home)
|
|
269
|
+
}
|
package/src/rpc.ts
CHANGED
|
@@ -34,7 +34,9 @@ import type { ContentSearcher } from './search/content.js'
|
|
|
34
34
|
import { newContentSearcher } from './search/content.js'
|
|
35
35
|
import { restoreFile, revertCall, revertHunk, revertHunks, type Result } from './revert.js'
|
|
36
36
|
import { rulesList, rulesRead, rulesRemove, rulesSave, rulesToggle } from './rules.js'
|
|
37
|
+
import { listSkillCatalog } from './skillCatalog.js'
|
|
37
38
|
import { isSnippetFilePath, snippetsEntries, snippetsList, snippetsRead, snippetsRemove, snippetsSave } from './snippets.js'
|
|
39
|
+
import { patchLauncher, readQuickMenu, saveQuickMenu } from './quickMenu.js'
|
|
38
40
|
import { runtimeOf } from './mcpRuntime.js'
|
|
39
41
|
import { listMcp, refreshMcp, removeMcp, saveMcp, toggleMcp } from './mcp.js'
|
|
40
42
|
import { listProjects, projectRefresh, projectRemove, projectSave, projectToggle, mcpSnapshot } from './mcpProject.js'
|
|
@@ -1565,6 +1567,14 @@ export function buildHandlers(
|
|
|
1565
1567
|
return { ok: false, error: '切换规则失败:' + String(error) }
|
|
1566
1568
|
}
|
|
1567
1569
|
},
|
|
1570
|
+
'skills.list': async (args) => {
|
|
1571
|
+
try {
|
|
1572
|
+
// 只读枚举:不做 workspace 注册校验(读盘无副作用),cwd 由客户端按会话作用域传入
|
|
1573
|
+
return { ok: true, ...(await listSkillCatalog(args?.cwd)) }
|
|
1574
|
+
} catch (error) {
|
|
1575
|
+
return { ok: false, error: '读取技能候选失败:' + String(error) }
|
|
1576
|
+
}
|
|
1577
|
+
},
|
|
1568
1578
|
'snippets.list': async () => {
|
|
1569
1579
|
try {
|
|
1570
1580
|
return { ok: true, ...(await snippetsList(ctx)) }
|
|
@@ -1604,6 +1614,33 @@ export function buildHandlers(
|
|
|
1604
1614
|
return { ok: false, error: '读取代码片段条目失败:' + String(error) }
|
|
1605
1615
|
}
|
|
1606
1616
|
},
|
|
1617
|
+
'quickMenu.get': async () => {
|
|
1618
|
+
try {
|
|
1619
|
+
const state = await readQuickMenu()
|
|
1620
|
+
return { ok: true, doc: state.doc, warning: state.warning }
|
|
1621
|
+
} catch (error) {
|
|
1622
|
+
return { ok: false, error: '读取快捷菜单失败:' + String(error) }
|
|
1623
|
+
}
|
|
1624
|
+
},
|
|
1625
|
+
'quickMenu.save': async (args) => {
|
|
1626
|
+
try {
|
|
1627
|
+
const result = await saveQuickMenu(args.doc, args.baseRevision)
|
|
1628
|
+
// 冲突时只回错误文案:客户端收到后重拉最新文档,避免用旧版本覆盖别人的写入
|
|
1629
|
+
if (!result.ok) return { ok: false, error: result.error }
|
|
1630
|
+
return { ok: true, doc: result.doc }
|
|
1631
|
+
} catch (error) {
|
|
1632
|
+
return { ok: false, error: '保存快捷菜单失败:' + String(error) }
|
|
1633
|
+
}
|
|
1634
|
+
},
|
|
1635
|
+
'quickMenu.launcher': async (args) => {
|
|
1636
|
+
try {
|
|
1637
|
+
const result = await patchLauncher({ x: args.x, y: args.y })
|
|
1638
|
+
if (!result.ok) return { ok: false, error: result.error }
|
|
1639
|
+
return { ok: true, doc: result.doc }
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
return { ok: false, error: '保存悬浮按钮位置失败:' + String(error) }
|
|
1642
|
+
}
|
|
1643
|
+
},
|
|
1607
1644
|
}
|
|
1608
1645
|
}
|
|
1609
1646
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 路径比较口径:Host 与客户端共用同一套「远端 / 盘符 / 相对路径」判定,只在平台折叠上分工。
|
|
3
|
+
* @author ddj 2026年10月30号
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** 远端仓库身份:`scheme://...` 区分大小写,绝不能折叠。 */
|
|
7
|
+
const REMOTE = /^[a-z]+:\/\//i
|
|
8
|
+
/** 盘符绝对路径:大小写不敏感与宿主平台无关,两边都必须折叠。 */
|
|
9
|
+
const DRIVE = /^[a-z]:\//i
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @author ddj 2026年10月30号
|
|
13
|
+
* @description: 统一分隔符为 `/`,其余字符原样保留。
|
|
14
|
+
* @param path 原始路径
|
|
15
|
+
* @returns 反斜杠已归一化的路径
|
|
16
|
+
*/
|
|
17
|
+
export function unifiedPath(path: string): string {
|
|
18
|
+
return path.replace(/\\/g, '/')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @author ddj 2026年10月30号
|
|
23
|
+
* @description: 是否为远端仓库身份。
|
|
24
|
+
* @param path 原始路径
|
|
25
|
+
* @returns 远端身份返回 true
|
|
26
|
+
*/
|
|
27
|
+
export function isRemotePath(path: string): boolean {
|
|
28
|
+
return REMOTE.test(unifiedPath(path))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @author ddj 2026年10月30号
|
|
33
|
+
* @description: 是否为盘符绝对路径。
|
|
34
|
+
* @param path 原始路径
|
|
35
|
+
* @returns Windows 盘符路径返回 true
|
|
36
|
+
*/
|
|
37
|
+
export function isDrivePath(path: string): boolean {
|
|
38
|
+
return DRIVE.test(unifiedPath(path))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @author ddj 2026年10月30号
|
|
43
|
+
* @description: 生成路径比较键。盘符路径恒折叠大小写;其余路径是否折叠由调用方按运行平台决定
|
|
44
|
+
* (Host 在 Windows 上折叠,客户端不在宿主进程里,取 false)。
|
|
45
|
+
* @param path 原始路径
|
|
46
|
+
* @param foldCase 非盘符路径是否折叠大小写
|
|
47
|
+
* @returns 比较键
|
|
48
|
+
*/
|
|
49
|
+
export function pathKeyFor(path: string, foldCase: boolean): string {
|
|
50
|
+
const unified = unifiedPath(path)
|
|
51
|
+
if (REMOTE.test(unified)) return unified
|
|
52
|
+
if (DRIVE.test(unified)) return unified.toLowerCase()
|
|
53
|
+
return foldCase ? unified.toLowerCase() : unified
|
|
54
|
+
}
|