dsh-vscode-mode 0.5.3 → 0.7.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 +72 -2
- package/lib/client.js +12082 -6420
- package/lib/client.js.map +1 -1
- package/lib/index.js +2750 -147
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/copyText.ts +28 -0
- package/src/client/dap/BpWidget.ts +262 -0
- package/src/client/dap/bpMenu.ts +91 -0
- package/src/client/dap/bpRowMenu.ts +53 -0
- package/src/client/dap/breakpoints.ts +222 -0
- package/src/client/dap/decorate.ts +128 -0
- package/src/client/dap/hintLine.ts +29 -0
- package/src/client/dap/hover.ts +166 -0
- package/src/client/dap/hoverMode.ts +159 -0
- package/src/client/dap/hoverTree.ts +722 -0
- package/src/client/dap/launchInsert.ts +229 -0
- package/src/client/dap/launchSnippetProvider.ts +206 -0
- package/src/client/dap/modelPath.ts +23 -0
- package/src/client/dap/panelSplit.ts +105 -0
- package/src/client/dap/pidPick.ts +24 -0
- package/src/client/dap/store.ts +571 -0
- package/src/client/dap/toolbarDrag.ts +51 -0
- package/src/client/dap/trace.ts +22 -0
- package/src/client/dap/variableTree.ts +62 -0
- package/src/client/fileClipboard.ts +46 -0
- package/src/client/index.ts +12 -3
- package/src/client/monaco/lsp/index.ts +15 -0
- package/src/client/monaco/lsp/providers.ts +2 -0
- package/src/client/monaco/theme.ts +15 -0
- package/src/client/openWithDialog.ts +66 -0
- package/src/client/searchSeed.ts +36 -0
- package/src/client/sidebar/contextMenu.ts +51 -10
- package/src/client/sidebar/menuItems.ts +473 -50
- package/src/client/sidebar/panels/DebugPanel.ts +613 -0
- package/src/client/sidebar/panels/SearchPanel.ts +24 -3
- package/src/client/sidebar/panels/SvnPanel.ts +167 -23
- package/src/client/sidebar/panels/index.ts +19 -0
- package/src/client/sidebar/types.ts +8 -0
- package/src/client/styles/editor.css +176 -0
- package/src/client/svnActions.ts +18 -1
- package/src/client/svnLog.ts +3 -3
- package/src/client/svnStatus.ts +140 -2
- package/src/client/tabActions.ts +22 -10
- package/src/client/ui/EditorView.ts +625 -23
- package/src/client/ui/OfficialSideTab.ts +1 -0
- package/src/client/ui/PromptDialog.ts +63 -0
- package/src/client/ui/SideBySideDiff.tsx +214 -29
- package/src/client/ui/SideEditorTab.ts +1 -0
- package/src/client/ui/SvnDiffPanel.ts +21 -8
- package/src/client/ui/SvnLogDialog.ts +44 -2
- package/src/client/ui/SvnPatchDialog.ts +63 -0
- package/src/client/ui/SvnSumDialog.ts +77 -0
- package/src/client/ui/commandCatalog.ts +63 -0
- package/src/client/ui/svnExport.ts +71 -0
- package/src/dap/configSnippets.ts +269 -0
- package/src/dap/discovery.ts +198 -0
- package/src/dap/launchConfig.ts +127 -0
- package/src/dap/manager.ts +588 -0
- package/src/dap/processList.ts +76 -0
- package/src/dap/protocol.ts +80 -0
- package/src/dap/provider.ts +49 -0
- package/src/dap/rpc.ts +192 -0
- package/src/dap/sourcePath.ts +82 -0
- package/src/index.ts +7 -3
- package/src/lsp/providers.ts +3 -2
- package/src/reveal.ts +31 -20
- package/src/rpc.ts +209 -5
- package/src/search/ripgrep.ts +127 -11
- package/src/shared/dap.ts +262 -0
- package/src/shared/fsNames.ts +115 -0
- package/src/shared/keybindings.ts +9 -0
- package/src/shared/rpc.ts +52 -2
- package/src/shared/svn.ts +147 -0
- package/src/shared/svnActions.ts +20 -0
- package/src/svn.ts +320 -11
- package/src/workspace.ts +0 -75
package/src/rpc.ts
CHANGED
|
@@ -20,8 +20,8 @@ import {
|
|
|
20
20
|
resolveTarget,
|
|
21
21
|
saveBucket,
|
|
22
22
|
} from './store.js'
|
|
23
|
-
import { mkdir, readFile, rm, stat, writeFile, readdir } from 'node:fs/promises'
|
|
24
|
-
import { basename, join } from 'node:path'
|
|
23
|
+
import { cp, mkdir, readFile, rename, rm, stat, writeFile, readdir } from 'node:fs/promises'
|
|
24
|
+
import { basename, dirname, join } from 'node:path'
|
|
25
25
|
import { archiveEntryFor, markDecision, recordResolved, reconstructOriginal } from './model.js'
|
|
26
26
|
import type { Registry } from './registry.js'
|
|
27
27
|
import { bucketOf, cwdOf, sessionOf } from './registry.js'
|
|
@@ -42,6 +42,7 @@ import { handoffOpen, pendingState, pollPending } from './externalHandoff.js'
|
|
|
42
42
|
import { buildReport } from './compat.js'
|
|
43
43
|
import { findProfileDir, readDevForm, setDevForm } from './devForm.js'
|
|
44
44
|
import { normalizeRel } from './tree.js'
|
|
45
|
+
import { baseNameOf, checkNewName, checkRenameName, isSubPath, joinRelPath, parentRelOf } from './shared/fsNames.js'
|
|
45
46
|
import { invalidateIndex, listDirCached } from './treeIndex.js'
|
|
46
47
|
import { revealInExplorer } from './reveal.js'
|
|
47
48
|
import { dshHome, debugLogFile, pluginLogRoot } from './paths.js'
|
|
@@ -246,6 +247,106 @@ async function integrationBaseUrlOf(ctx: Ctx): Promise<string> {
|
|
|
246
247
|
return raw || INTEGRATION_BASE_DEFAULT
|
|
247
248
|
}
|
|
248
249
|
|
|
250
|
+
// --region 文件操作(edrv.fs.*:资源管理器右键的新建/重命名/删除/复制/移动)
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 文件操作目标解析:工作区相对路径 → 绝对路径,并做工作区边界检查。
|
|
254
|
+
* edrv.fs.* 各方法共用(对齐 edrv.saveBinary 的 contains 护栏口径)。
|
|
255
|
+
* @author ddj 2026年09月22号
|
|
256
|
+
* @param ctx DSH 上下文
|
|
257
|
+
* @param session 当前会话
|
|
258
|
+
* @param rel 工作区相对路径
|
|
259
|
+
* @returns 绝对路径;缺 fs/越界/解析失败返回错误文案
|
|
260
|
+
*/
|
|
261
|
+
async function fsOpsTarget(ctx: Ctx, session: Session, rel: string): Promise<{ abs: string } | { err: string }> {
|
|
262
|
+
const fs = ctx.get('fs')
|
|
263
|
+
if (!fs) return { err: '缺少 fs' }
|
|
264
|
+
try {
|
|
265
|
+
const target = await resolveTarget(ctx, session, rel)
|
|
266
|
+
const rootTarget = await fs.resolve(policyOf(ctx, session)?.workspaceRoot ?? '.', {})
|
|
267
|
+
if (!fs.contains(rootTarget, target)) return { err: '拒绝操作:目标不在会话工作区内' }
|
|
268
|
+
return { abs: fs.processPath(target) }
|
|
269
|
+
} catch (error) {
|
|
270
|
+
return { err: '路径解析失败:' + String(error) }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 取异常的系统错误码(EEXIST/EXDEV 等分支判定用)。
|
|
276
|
+
* @author ddj 2026年09月22号
|
|
277
|
+
* @param error 捕获到的异常
|
|
278
|
+
* @returns 错误码;取不到返回空串
|
|
279
|
+
*/
|
|
280
|
+
function fsErrCode(error: unknown): string {
|
|
281
|
+
const code = (error as { code?: unknown } | null)?.code
|
|
282
|
+
return typeof code === 'string' ? code : ''
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 复制/移动共用核心(edrv.fsCopy / edrv.fsMove):目标目录存在 + 同名拒绝覆盖 +
|
|
287
|
+
* 「不能以自身或其子目录为目标」护栏;move 跨设备(EXDEV)回退「递归复制 + 删除源」。
|
|
288
|
+
* @author ddj 2026年09月22号
|
|
289
|
+
* @param ctx DSH 上下文
|
|
290
|
+
* @param sc requireSession 成功结果(session + cwd)
|
|
291
|
+
* @param args 请求载荷(from / toDir 均为工作区相对路径)
|
|
292
|
+
* @param mode copy = 复制;move = 移动
|
|
293
|
+
* @returns 成功返回 from/to 相对路径;失败返回错误文案
|
|
294
|
+
*/
|
|
295
|
+
async function fsTransfer(
|
|
296
|
+
ctx: Ctx,
|
|
297
|
+
sc: { session: Session; cwd: string },
|
|
298
|
+
args: { from: string; toDir: string },
|
|
299
|
+
mode: 'copy' | 'move',
|
|
300
|
+
): Promise<{ from: string; to: string } | { err: string }> {
|
|
301
|
+
const fromRel = normalizeRel(args.from)
|
|
302
|
+
const toDirRel = normalizeRel(args.toDir)
|
|
303
|
+
if (fromRel === null || fromRel === '' || toDirRel === null) return { err: '路径不合法' }
|
|
304
|
+
if (isSubPath(fromRel, toDirRel)) return { err: '不能以自身或其子目录为目标' }
|
|
305
|
+
const src = await fsOpsTarget(ctx, sc.session, fromRel)
|
|
306
|
+
if ('err' in src) return src
|
|
307
|
+
const dir = await fsOpsTarget(ctx, sc.session, toDirRel)
|
|
308
|
+
if ('err' in dir) return dir
|
|
309
|
+
const dirInfo = await stat(dir.abs).catch(() => null)
|
|
310
|
+
if (!dirInfo || !dirInfo.isDirectory()) return { err: '目标目录不存在' }
|
|
311
|
+
const toRel = joinRelPath(toDirRel, baseNameOf(fromRel))
|
|
312
|
+
const dst = await fsOpsTarget(ctx, sc.session, toRel)
|
|
313
|
+
if ('err' in dst) return dst
|
|
314
|
+
const srcInfo = await stat(src.abs).catch(() => null)
|
|
315
|
+
if (!srcInfo) return { err: '源不存在' }
|
|
316
|
+
if (await stat(dst.abs).catch(() => null)) return { err: '目标目录已存在同名文件或目录' }
|
|
317
|
+
try {
|
|
318
|
+
await transferEntry(src.abs, dst.abs, mode)
|
|
319
|
+
} catch (error) {
|
|
320
|
+
return { err: (mode === 'copy' ? '复制失败:' : '移动失败:') + String(error) }
|
|
321
|
+
}
|
|
322
|
+
invalidateIndex(ctx, sc.cwd, fromRel)
|
|
323
|
+
invalidateIndex(ctx, sc.cwd, toRel)
|
|
324
|
+
return { from: fromRel, to: toRel }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* 单条目复制/移动落盘(文件/目录通用;move 跨设备回退复制+删除源)。
|
|
329
|
+
* @author ddj 2026年09月22号
|
|
330
|
+
* @param absFrom 源绝对路径
|
|
331
|
+
* @param absTo 目标绝对路径
|
|
332
|
+
* @param mode copy = 复制;move = 移动
|
|
333
|
+
*/
|
|
334
|
+
async function transferEntry(absFrom: string, absTo: string, mode: 'copy' | 'move'): Promise<void> {
|
|
335
|
+
if (mode === 'copy') {
|
|
336
|
+
await cp(absFrom, absTo, { recursive: true, force: false, errorOnExist: true })
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
await rename(absFrom, absTo)
|
|
341
|
+
} catch (error) {
|
|
342
|
+
if (fsErrCode(error) !== 'EXDEV') throw error
|
|
343
|
+
await cp(absFrom, absTo, { recursive: true, force: false, errorOnExist: true })
|
|
344
|
+
await rm(absFrom, { recursive: true })
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// --endregion
|
|
349
|
+
|
|
249
350
|
/** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
|
|
250
351
|
export function buildHandlers(
|
|
251
352
|
ctx: Ctx,
|
|
@@ -256,6 +357,7 @@ export function buildHandlers(
|
|
|
256
357
|
aiHandlers?: Partial<RpcHandlerMap>,
|
|
257
358
|
fileVersions?: FileVersions,
|
|
258
359
|
svnHandlers?: Partial<RpcHandlerMap>,
|
|
360
|
+
dapHandlers?: Partial<RpcHandlerMap>,
|
|
259
361
|
): RpcHandlerMap {
|
|
260
362
|
return {
|
|
261
363
|
// edrv.lsp.* 由 createLspRpc 一次性提供(tracker 跨请求保留),这里并入。
|
|
@@ -264,6 +366,8 @@ export function buildHandlers(
|
|
|
264
366
|
...((aiHandlers ?? {}) as RpcHandlerMap),
|
|
265
367
|
// svn.* 由 createSvnRpc 提供(SVN 检测/更新/Tortoise 发射),这里并入。
|
|
266
368
|
...((svnHandlers ?? {}) as RpcHandlerMap),
|
|
369
|
+
// edrv.dap.* 由 createDapRpc 提供(调试会话单例),这里并入。
|
|
370
|
+
...((dapHandlers ?? {}) as RpcHandlerMap),
|
|
267
371
|
'edrv.list': async (args) => {
|
|
268
372
|
const sc = await requireSession(ctx, args.sessionId)
|
|
269
373
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
@@ -333,6 +437,7 @@ export function buildHandlers(
|
|
|
333
437
|
if (!fs) return { ok: false, error: '缺少 fs' }
|
|
334
438
|
try {
|
|
335
439
|
const target = await resolveTarget(ctx, sc.session, args.path)
|
|
440
|
+
debugRecord(ctx, sc.cwd, '[DEBUG path.resolve] input=' + String(args.path ?? '') + ' resolved=' + fs.processPath(target), 'debug')
|
|
336
441
|
const info = await fs.stat(target)
|
|
337
442
|
if (!info || info.type !== 'file') {
|
|
338
443
|
// 带上解析后的真实路径:跳转失败时可直接看出是路径解析错还是目标本身不存在
|
|
@@ -366,6 +471,7 @@ export function buildHandlers(
|
|
|
366
471
|
if (!fs) return { ok: false, error: '缺少 fs' }
|
|
367
472
|
try {
|
|
368
473
|
const target = await resolveTarget(ctx, sc.session, args.path)
|
|
474
|
+
debugRecord(ctx, sc.cwd, '[DEBUG path.resolve] input=' + String(args.path ?? '') + ' resolved=' + fs.processPath(target), 'debug')
|
|
369
475
|
const info = await fs.stat(target)
|
|
370
476
|
if (!info || info.type !== 'file') return { ok: false, error: '文件不存在' }
|
|
371
477
|
if ((info.size ?? 0) > READ_CAP) return { ok: false, error: '文件过大(>8MB),不支持差异重建' }
|
|
@@ -399,6 +505,7 @@ export function buildHandlers(
|
|
|
399
505
|
if (!fs) return { ok: false, error: '缺少 fs' }
|
|
400
506
|
try {
|
|
401
507
|
const target = await resolveTarget(ctx, sc.session, args.path)
|
|
508
|
+
debugRecord(ctx, sc.cwd, '[DEBUG path.resolve] input=' + String(args.path ?? '') + ' resolved=' + fs.processPath(target), 'debug')
|
|
402
509
|
// 版本守卫(客户端带回上次读取的版本):文件在读取后被外部改过则拒绝写入,
|
|
403
510
|
// 避免用陈旧缓冲静默覆盖外部改动;客户端未带 rev(旧版/无版本后端)时行为不变。
|
|
404
511
|
const rev = typeof args.rev === 'string' && args.rev ? args.rev : null
|
|
@@ -421,6 +528,7 @@ export function buildHandlers(
|
|
|
421
528
|
if (!fs) return { ok: false, error: '缺少 fs' }
|
|
422
529
|
try {
|
|
423
530
|
const target = await resolveTarget(ctx, sc.session, args.path)
|
|
531
|
+
debugRecord(ctx, sc.cwd, '[DEBUG path.resolve] input=' + String(args.path ?? '') + ' resolved=' + fs.processPath(target), 'debug')
|
|
424
532
|
const info = await fs.stat(target)
|
|
425
533
|
if (!info || info.type !== 'file') return { ok: false, error: '文件不存在' }
|
|
426
534
|
// fs 服务仅提供文本写(writeText/editText),二进制回写经 node:fs 直写解析后的
|
|
@@ -540,7 +648,7 @@ export function buildHandlers(
|
|
|
540
648
|
},
|
|
541
649
|
'edrv.dlog.reveal': async () => {
|
|
542
650
|
// 打开日志根目录(OS 文件管理器;目录形态 → 直接打开)
|
|
543
|
-
const revealed = await revealInExplorer(
|
|
651
|
+
const revealed = await revealInExplorer(pluginLogRoot(), true)
|
|
544
652
|
if (!revealed.ok) return { ok: false, error: revealed.error }
|
|
545
653
|
return { ok: true, opened: true }
|
|
546
654
|
},
|
|
@@ -598,13 +706,108 @@ export function buildHandlers(
|
|
|
598
706
|
const info = await fs.stat(target)
|
|
599
707
|
if (!info) return { ok: false, error: '路径不存在' }
|
|
600
708
|
const abs = fs.processPath(target)
|
|
601
|
-
const outcome = await revealInExplorer(
|
|
709
|
+
const outcome = await revealInExplorer(abs, info.type === 'directory')
|
|
602
710
|
if (!outcome.ok) return { ok: false, error: outcome.error }
|
|
603
711
|
return { ok: true, revealed: abs }
|
|
604
712
|
} catch (error) {
|
|
605
713
|
return { ok: false, error: '打开失败:' + String(error) }
|
|
606
714
|
}
|
|
607
715
|
},
|
|
716
|
+
'edrv.fsCreateFile': async (args) => {
|
|
717
|
+
// 新建文件(资源管理器右键「新建文件…」):空内容独占创建(同名拒绝),
|
|
718
|
+
// 名称可含 a/b.c 嵌套段(父目录按需创建);名称校验与客户端弹窗共用 shared/fsNames。
|
|
719
|
+
const sc = await requireSession(ctx, args.sessionId)
|
|
720
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
721
|
+
const bad = checkNewName(args.path)
|
|
722
|
+
if (bad) return { ok: false, error: bad }
|
|
723
|
+
const r = await fsOpsTarget(ctx, sc.session, args.path)
|
|
724
|
+
if ('err' in r) return { ok: false, error: r.err }
|
|
725
|
+
try {
|
|
726
|
+
await mkdir(dirname(r.abs), { recursive: true })
|
|
727
|
+
await writeFile(r.abs, '', { flag: 'wx' })
|
|
728
|
+
} catch (error) {
|
|
729
|
+
const code = fsErrCode(error)
|
|
730
|
+
return { ok: false, error: code === 'EEXIST' ? '已存在同名文件或目录' : '新建文件失败:' + String(error) }
|
|
731
|
+
}
|
|
732
|
+
invalidateIndex(ctx, sc.cwd, args.path)
|
|
733
|
+
return { ok: true, path: args.path }
|
|
734
|
+
},
|
|
735
|
+
'edrv.fsCreateDir': async (args) => {
|
|
736
|
+
// 新建文件夹(资源管理器右键「新建文件夹」):名称可含嵌套段(父目录按需创建),已存在拒绝。
|
|
737
|
+
const sc = await requireSession(ctx, args.sessionId)
|
|
738
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
739
|
+
const bad = checkNewName(args.path)
|
|
740
|
+
if (bad) return { ok: false, error: bad }
|
|
741
|
+
const r = await fsOpsTarget(ctx, sc.session, args.path)
|
|
742
|
+
if ('err' in r) return { ok: false, error: r.err }
|
|
743
|
+
try {
|
|
744
|
+
await mkdir(dirname(r.abs), { recursive: true })
|
|
745
|
+
await mkdir(r.abs)
|
|
746
|
+
} catch (error) {
|
|
747
|
+
const code = fsErrCode(error)
|
|
748
|
+
return { ok: false, error: code === 'EEXIST' ? '已存在同名文件或目录' : '新建文件夹失败:' + String(error) }
|
|
749
|
+
}
|
|
750
|
+
invalidateIndex(ctx, sc.cwd, args.path)
|
|
751
|
+
return { ok: true, path: args.path }
|
|
752
|
+
},
|
|
753
|
+
'edrv.fsRename': async (args) => {
|
|
754
|
+
// 重命名(仅名称段、同父目录内):目标已存在拒绝;名称未变化按幂等成功返回。
|
|
755
|
+
const sc = await requireSession(ctx, args.sessionId)
|
|
756
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
757
|
+
const bad = checkRenameName(args.newName)
|
|
758
|
+
if (bad) return { ok: false, error: bad }
|
|
759
|
+
const fromRel = normalizeRel(args.path)
|
|
760
|
+
if (fromRel === null || fromRel === '') return { ok: false, error: '路径不合法' }
|
|
761
|
+
const toRel = joinRelPath(parentRelOf(fromRel), args.newName)
|
|
762
|
+
if (toRel === fromRel) return { ok: true, from: fromRel, to: fromRel }
|
|
763
|
+
const src = await fsOpsTarget(ctx, sc.session, fromRel)
|
|
764
|
+
if ('err' in src) return { ok: false, error: src.err }
|
|
765
|
+
const dst = await fsOpsTarget(ctx, sc.session, toRel)
|
|
766
|
+
if ('err' in dst) return { ok: false, error: dst.err }
|
|
767
|
+
if (!(await stat(src.abs).catch(() => null))) return { ok: false, error: '源不存在' }
|
|
768
|
+
try {
|
|
769
|
+
await rename(src.abs, dst.abs)
|
|
770
|
+
} catch (error) {
|
|
771
|
+
const code = fsErrCode(error)
|
|
772
|
+
const exists = code === 'EEXIST' || code === 'ENOTEMPTY' || code === 'EPERM'
|
|
773
|
+
return { ok: false, error: exists ? '已存在同名文件或目录' : '重命名失败:' + String(error) }
|
|
774
|
+
}
|
|
775
|
+
invalidateIndex(ctx, sc.cwd, fromRel)
|
|
776
|
+
invalidateIndex(ctx, sc.cwd, toRel)
|
|
777
|
+
return { ok: true, from: fromRel, to: toRel }
|
|
778
|
+
},
|
|
779
|
+
'edrv.fsDelete': async (args) => {
|
|
780
|
+
// 删除(文件/文件夹递归;「删除/永久删除」共用):根拒绝;破坏性由客户端确认框把关。
|
|
781
|
+
const sc = await requireSession(ctx, args.sessionId)
|
|
782
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
783
|
+
const rel = normalizeRel(args.path)
|
|
784
|
+
if (rel === null || rel === '') return { ok: false, error: '路径不合法' }
|
|
785
|
+
const r = await fsOpsTarget(ctx, sc.session, rel)
|
|
786
|
+
if ('err' in r) return { ok: false, error: r.err }
|
|
787
|
+
try {
|
|
788
|
+
await rm(r.abs, { recursive: true })
|
|
789
|
+
} catch (error) {
|
|
790
|
+
return { ok: false, error: '删除失败:' + String(error) }
|
|
791
|
+
}
|
|
792
|
+
invalidateIndex(ctx, sc.cwd, rel)
|
|
793
|
+
return { ok: true, path: rel }
|
|
794
|
+
},
|
|
795
|
+
'edrv.fsCopy': async (args) => {
|
|
796
|
+
// 复制到目标目录(文件/文件夹递归):同名拒绝覆盖(细节见 fsTransfer)。
|
|
797
|
+
const sc = await requireSession(ctx, args.sessionId)
|
|
798
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
799
|
+
const r = await fsTransfer(ctx, sc, args, 'copy')
|
|
800
|
+
if ('err' in r) return { ok: false, error: r.err }
|
|
801
|
+
return { ok: true, from: r.from, to: r.to }
|
|
802
|
+
},
|
|
803
|
+
'edrv.fsMove': async (args) => {
|
|
804
|
+
// 移动到目标目录(文件/文件夹):同名拒绝覆盖(细节见 fsTransfer)。
|
|
805
|
+
const sc = await requireSession(ctx, args.sessionId)
|
|
806
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
807
|
+
const r = await fsTransfer(ctx, sc, args, 'move')
|
|
808
|
+
if ('err' in r) return { ok: false, error: r.err }
|
|
809
|
+
return { ok: true, from: r.from, to: r.to }
|
|
810
|
+
},
|
|
608
811
|
'mcp.list': async () => ({ ok: true, ...listMcp(ctx) }),
|
|
609
812
|
'mcp.save': async (args) => {
|
|
610
813
|
try { return { ok: true, server: await saveMcp(ctx, args.config) } }
|
|
@@ -951,8 +1154,9 @@ export async function handleRpc<M extends RpcMethod>(
|
|
|
951
1154
|
aiHandlers?: Partial<RpcHandlerMap>,
|
|
952
1155
|
fileVersions?: FileVersions,
|
|
953
1156
|
svnHandlers?: Partial<RpcHandlerMap>,
|
|
1157
|
+
dapHandlers?: Partial<RpcHandlerMap>,
|
|
954
1158
|
): Promise<RpcResult<M>> {
|
|
955
|
-
const handlers = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers, aiHandlers, fileVersions, svnHandlers)
|
|
1159
|
+
const handlers = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers, aiHandlers, fileVersions, svnHandlers, dapHandlers)
|
|
956
1160
|
const handler = handlers[method]
|
|
957
1161
|
if (!handler) return { ok: false, error: '未知方法: ' + String(method) } as RpcResult<M>
|
|
958
1162
|
return handler(args)
|
package/src/search/ripgrep.ts
CHANGED
|
@@ -15,7 +15,12 @@ const STDOUT_CAP = 4 << 20
|
|
|
15
15
|
const STDERR_CAP = 64 << 10
|
|
16
16
|
const GRACE_MS = 20_000
|
|
17
17
|
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* 默认排除目录(文件/内容搜索共用,导出供 provider 复用)。
|
|
20
|
+
* 语义为**大小写敏感**的路径段匹配:`build` 只排除小写构建产物目录,
|
|
21
|
+
* 不得用 `--glob-case-insensitive` 之类的全局开关,「Module/Build」这类
|
|
22
|
+
* 源码目录(如 IslandSplash 的 Module/Build/Logic)必须保持可搜索。
|
|
23
|
+
*/
|
|
19
24
|
export const EXCLUDES = [
|
|
20
25
|
'node_modules', '.git', '.tmp', '.cache', 'dist', 'build', 'vendor',
|
|
21
26
|
'coverage', '__pycache__', '.pnpm-store', '.npm-cache', '.codegraph', '.workbuddy',
|
|
@@ -52,15 +57,32 @@ interface SearchSubprocess {
|
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
60
|
+
* 将查询文本转成大小写不敏感的 rg 正选 glob。
|
|
61
|
+
* 逐字符把有大小写的字母展开成 `[aA]` 字符类,**不用**大小写不敏感开关:
|
|
62
|
+
* ① 全局 `--glob-case-insensitive` 会把 `-g/--glob` 全部当成 `--iglob`,
|
|
63
|
+
* 连带让 `!**\/build/**` 大小写不敏感,从而排除 `Module/Build/` 这类源码目录
|
|
64
|
+
* (Ctrl+P 搜 BuildLogic 恒「无匹配文件」);
|
|
65
|
+
* ② 把查询词改用 `--iglob` 也不行——rg 的正选 glob 会覆盖全部黑名单 glob,
|
|
66
|
+
* node_modules/.git 等排除项集体失效。
|
|
67
|
+
* 字符类方案同时满足「查询不区分大小写」且「排除项保持大小写敏感」。
|
|
68
|
+
* @author ddj 2026年08月24号 / 2026年09月20号
|
|
69
|
+
* @param query 已规范化查询文本
|
|
58
70
|
* @returns 安全 glob
|
|
59
71
|
*/
|
|
60
72
|
export function queryGlob(query: string): string {
|
|
61
73
|
const value = pathText(query)
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
let body = ''
|
|
75
|
+
for (const char of value) {
|
|
76
|
+
const lower = char.toLowerCase()
|
|
77
|
+
const upper = char.toUpperCase()
|
|
78
|
+
// 有大小写之分的字符(单字符映射,避开 ß→SS 这类长度变化)→ 字符类
|
|
79
|
+
if (lower !== upper && lower.length === 1 && upper.length === 1) {
|
|
80
|
+
body += '[' + lower + upper + ']'
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
body += /[\*?\[\]{}!]/.test(char) ? '\\' + char : char
|
|
84
|
+
}
|
|
85
|
+
return '**/*' + body + '*'
|
|
64
86
|
}
|
|
65
87
|
|
|
66
88
|
/**
|
|
@@ -159,6 +181,24 @@ export function rgExitFailure(code: number | null | undefined, stderrText: strin
|
|
|
159
181
|
return { kind: 'hard', message: 'ripgrep 退出码:' + String(code) + (detail ? '(' + detail + ')' : '') }
|
|
160
182
|
}
|
|
161
183
|
|
|
184
|
+
/**
|
|
185
|
+
* 构造文件搜索 rg argv(查询 glob 大小写不敏感,排除 glob 大小写敏感)。
|
|
186
|
+
* 大小写不敏感**只能**靠 queryGlob 的字符类实现:全局 `--glob-case-insensitive`
|
|
187
|
+
* 会波及排除项(误排除 `Module/Build/`),查询词用 `--iglob` 又会覆盖全部黑名单 glob。
|
|
188
|
+
* @author ddj 2026年09月20号
|
|
189
|
+
* @param binary rg 可执行文件路径
|
|
190
|
+
* @param root 搜索根目录
|
|
191
|
+
* @param query 已规范化查询文本
|
|
192
|
+
* @returns 完整 argv(末位为根目录)
|
|
193
|
+
*/
|
|
194
|
+
export function filesArgv(binary: string, root: string, query: string): string[] {
|
|
195
|
+
const argv = [binary, '--no-config', '--files', '--hidden', '--no-ignore', '--glob', queryGlob(query)]
|
|
196
|
+
for (const excluded of EXCLUDES) argv.push('--glob', '!**/' + excluded + '/**')
|
|
197
|
+
for (const excluded of FILE_EXCLUDES) argv.push('--glob', '!' + excluded)
|
|
198
|
+
argv.push('--', root)
|
|
199
|
+
return argv
|
|
200
|
+
}
|
|
201
|
+
|
|
162
202
|
/**
|
|
163
203
|
* 使用打包 ripgrep 搜索工作区文件。
|
|
164
204
|
* @author ddj 2026年08月24号
|
|
@@ -172,12 +212,8 @@ export async function searchRipgrep(input: WorkspaceSearchInput): Promise<Worksp
|
|
|
172
212
|
if (!binary) throw new Error('ripgrep 不可用')
|
|
173
213
|
const root = input.root ?? await searchRoot(input.ctx, input.session)
|
|
174
214
|
const query = prepareQuery(input.query)
|
|
175
|
-
const argv = [binary, '--no-config', '--files', '--hidden', '--no-ignore', '--glob-case-insensitive', '--glob', queryGlob(query.text)]
|
|
176
|
-
for (const excluded of EXCLUDES) argv.push('--glob', '!**/' + excluded + '/**')
|
|
177
|
-
for (const excluded of FILE_EXCLUDES) argv.push('--glob', '!' + excluded)
|
|
178
|
-
argv.push('--', root)
|
|
179
215
|
const handle = sub.spawn({
|
|
180
|
-
argv,
|
|
216
|
+
argv: filesArgv(binary, root, query.text),
|
|
181
217
|
cwd: root,
|
|
182
218
|
stdio: { stdin: 'ignore', stdout: { maxBytes: STDOUT_CAP }, stderr: { maxBytes: STDERR_CAP } },
|
|
183
219
|
graceMs: GRACE_MS,
|
|
@@ -202,6 +238,86 @@ export async function searchRipgrep(input: WorkspaceSearchInput): Promise<Worksp
|
|
|
202
238
|
return { ...parsed, source: 'ripgrep' }
|
|
203
239
|
}
|
|
204
240
|
|
|
241
|
+
/**
|
|
242
|
+
* 构造 chunkname 定向查找的 rg argv。
|
|
243
|
+
* 与 {@link filesArgv} 同源(同样的排除 glob 与大小写口径),差异只在查询 glob 由
|
|
244
|
+
* chunkname 推导:chunkname 可能带 chunk 前缀(`@`)与无扩展名 stem,故先剥 `@`、
|
|
245
|
+
* 取 basename、去掉扩展名后作为片段匹配,仍走 {@link queryGlob} 的字符类方案
|
|
246
|
+
* ——避免 `--glob-case-insensitive` 波及排除项(见 queryGlob 注释)。
|
|
247
|
+
* @author ddj 2026年09月21号
|
|
248
|
+
* @param binary rg 可执行文件路径
|
|
249
|
+
* @param root 搜索根目录
|
|
250
|
+
* @param chunk 适配器上报的 chunkname
|
|
251
|
+
* @returns 完整 argv(末位为根目录);chunk 为空时返回 null
|
|
252
|
+
*/
|
|
253
|
+
export function chunkFilesArgv(binary: string, root: string, chunk: string): string[] | null {
|
|
254
|
+
const clean = normalizeChunk(chunk)
|
|
255
|
+
if (!clean) return null
|
|
256
|
+
const argv = [binary, '--no-config', '--files', '--hidden', '--no-ignore', '--glob', queryGlob(clean)]
|
|
257
|
+
for (const excluded of EXCLUDES) argv.push('--glob', '!**/' + excluded + '/**')
|
|
258
|
+
for (const excluded of FILE_EXCLUDES) argv.push('--glob', '!' + excluded)
|
|
259
|
+
argv.push('--', root)
|
|
260
|
+
return argv
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* chunkname → 用于片段匹配的纯文件名片段。
|
|
265
|
+
* 剥 `@` 前缀、统一分隔符、取 basename、去掉扩展名(xLua chunkname 惯例不含扩展名),
|
|
266
|
+
* 使 `GuideSystemMgr` 能命中 `GuideSystemMgr.lua`。
|
|
267
|
+
* @author ddj 2026年09月21号
|
|
268
|
+
* @param chunk 适配器上报的 chunkname
|
|
269
|
+
* @returns 纯片段(空输入返回空串)
|
|
270
|
+
*/
|
|
271
|
+
export function normalizeChunk(chunk: string): string {
|
|
272
|
+
const text = String(chunk ?? '').replace(/^@/, '').replaceAll('\\', '/').trim()
|
|
273
|
+
if (!text) return ''
|
|
274
|
+
const base = text.split('/').pop() ?? ''
|
|
275
|
+
return base.replace(/\.[^.]+$/, '')
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 按 chunkname 定向查找工作区文件(DAP findFile 反向匹配专用)。
|
|
280
|
+
*
|
|
281
|
+
* 为什么不用工作区全量文件索引:22 万文件的 Unity 工程里,全量枚举按到达序截断,
|
|
282
|
+
* `Assets/Scripts/Lua/**` 会被 `GameData`/`Entitas` 等目录挤出配额,导致断点命中后
|
|
283
|
+
* 无法定位源文件(实测前 6000 条中 .lua 数为 0)。此处改为让 rg 在遍历时过滤,
|
|
284
|
+
* 命中规模与工作区总规模无关(实测同一工作区 0.37s 返回正确结果)。
|
|
285
|
+
* @author ddj 2026年09月21号
|
|
286
|
+
* @param ctx DSH 上下文
|
|
287
|
+
* @param root 工作区根(subprocess 可访问路径)
|
|
288
|
+
* @param chunk 适配器上报的 chunkname
|
|
289
|
+
* @param maxResults 候选上限
|
|
290
|
+
* @returns 绝对路径候选(任何失败都返回空数组,不抛错)
|
|
291
|
+
*/
|
|
292
|
+
export async function findFilesByChunk(ctx: Ctx, root: string, chunk: string, maxResults = 50): Promise<string[]> {
|
|
293
|
+
const sub = ctx.get('subprocess') as SearchSubprocess | undefined
|
|
294
|
+
const binary = ripgrepPath()
|
|
295
|
+
if (!sub || !binary || !root) return []
|
|
296
|
+
const argv = chunkFilesArgv(binary, root, chunk)
|
|
297
|
+
if (!argv) return []
|
|
298
|
+
let handle: SearchHandle
|
|
299
|
+
try {
|
|
300
|
+
handle = sub.spawn({
|
|
301
|
+
argv,
|
|
302
|
+
cwd: root,
|
|
303
|
+
stdio: { stdin: 'ignore', stdout: { maxBytes: STDOUT_CAP }, stderr: { maxBytes: STDERR_CAP } },
|
|
304
|
+
graceMs: GRACE_MS,
|
|
305
|
+
})
|
|
306
|
+
} catch (error) {
|
|
307
|
+
return []
|
|
308
|
+
}
|
|
309
|
+
let code: number | null | undefined
|
|
310
|
+
try {
|
|
311
|
+
const outcome = await handle.done
|
|
312
|
+
code = outcome.exitCode ?? outcome.code
|
|
313
|
+
} catch (error) {
|
|
314
|
+
return []
|
|
315
|
+
}
|
|
316
|
+
// 0=有命中 1=无命中 2=部分不可访问(已收集结果仍有效):其余视为失败
|
|
317
|
+
if (code !== 0 && code !== 1 && code !== 2) return []
|
|
318
|
+
return parseOutput(handle, maxResults).files
|
|
319
|
+
}
|
|
320
|
+
|
|
205
321
|
/**
|
|
206
322
|
* 构造 ripgrep provider。
|
|
207
323
|
* @author ddj 2026年08月24号
|