dsh-vscode-mode 0.1.12 → 0.1.14
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/lib/index.js +607 -88
- package/lib/index.js.map +1 -1
- package/package.json +4 -1
- package/src/index.ts +4 -1
- package/src/rpc.ts +10 -24
- package/src/search/fallback.ts +76 -0
- package/src/search/orchestrator.ts +276 -0
- package/src/search/query.ts +62 -0
- package/src/search/ranker.ts +48 -0
- package/src/search/ripgrep.ts +144 -0
- package/src/search/types.ts +47 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — ripgrep 文件发现 provider。
|
|
3
|
+
* 通过独立 argv 调用打包 rg,避免先构建巨型全量文件数组。
|
|
4
|
+
* 作者 ddj 2026年08月24号
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from 'node:fs'
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
import { createRequire } from 'node:module'
|
|
9
|
+
import type { Ctx, Session } from '../store.js'
|
|
10
|
+
import { policyOf } from '../store.js'
|
|
11
|
+
import { prepareQuery, pathText } from './query.js'
|
|
12
|
+
import type { WorkspaceSearchInput, WorkspaceSearchProvider, WorkspaceSearchResult } from './types.js'
|
|
13
|
+
|
|
14
|
+
const STDOUT_CAP = 4 << 20
|
|
15
|
+
const STDERR_CAP = 64 << 10
|
|
16
|
+
const GRACE_MS = 20_000
|
|
17
|
+
const EXCLUDES = [
|
|
18
|
+
'node_modules', '.git', '.tmp', '.cache', 'dist', 'build', 'vendor',
|
|
19
|
+
'coverage', '__pycache__',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
interface SearchHandle {
|
|
23
|
+
done: Promise<{ exitCode: number | null; code?: number | null }>
|
|
24
|
+
collected?: {
|
|
25
|
+
stdout?: { readFrom(offset: number): { text: string; lossy?: boolean } }
|
|
26
|
+
stderr?: { readFrom(offset: number): { text: string } }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface SearchSubprocess {
|
|
31
|
+
spawn(spec: {
|
|
32
|
+
argv: readonly string[]
|
|
33
|
+
cwd: string
|
|
34
|
+
stdio: { stdin: 'ignore'; stdout: { maxBytes: number }; stderr: { maxBytes: number } }
|
|
35
|
+
graceMs: number
|
|
36
|
+
signal?: AbortSignal
|
|
37
|
+
}): SearchHandle
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 将 query 转成 rg 的字面 glob。
|
|
42
|
+
* @author ddj 2026年08月24号
|
|
43
|
+
* @param query 已规范化 query
|
|
44
|
+
* @returns 安全 glob
|
|
45
|
+
*/
|
|
46
|
+
export function queryGlob(query: string): string {
|
|
47
|
+
const value = pathText(query)
|
|
48
|
+
const escaped = value.replace(/[\*?\[\]{}!]/g, (char) => '\\' + char)
|
|
49
|
+
return '**/*' + escaped + '*'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 返回当前平台的 rg 可执行文件路径,不让可选依赖影响模块加载。
|
|
54
|
+
* @author ddj 2026年08月24号
|
|
55
|
+
* @returns rg 路径或 null
|
|
56
|
+
*/
|
|
57
|
+
export function ripgrepPath(): string | null {
|
|
58
|
+
try {
|
|
59
|
+
const require = createRequire(import.meta.url)
|
|
60
|
+
const ripgrep = require('@vscode/ripgrep') as { rgPath?: unknown }
|
|
61
|
+
if (typeof ripgrep.rgPath !== 'string' || !existsSync(ripgrep.rgPath)) return null
|
|
62
|
+
return join(ripgrep.rgPath)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 解析会话搜索根目录,并转换到 subprocess 执行世界。
|
|
70
|
+
* @author ddj 2026年08月24号
|
|
71
|
+
* @param ctx DSH 上下文
|
|
72
|
+
* @param session 当前会话
|
|
73
|
+
* @returns subprocess 可访问的根目录
|
|
74
|
+
*/
|
|
75
|
+
export async function searchRoot(ctx: Ctx, session: Session): Promise<string> {
|
|
76
|
+
const fs = ctx.get('fs')
|
|
77
|
+
if (!fs) throw new Error('缺少 fs')
|
|
78
|
+
const policy = policyOf(ctx, session)
|
|
79
|
+
const cwd = session?.header?.cwd
|
|
80
|
+
const rootTarget = await fs.resolve(policy?.workspaceRoot ?? '.', cwd ? { cwd } : {})
|
|
81
|
+
return fs.processPath(rootTarget)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 读取收集输出并解析为路径候选。
|
|
86
|
+
* @author ddj 2026年08月24号
|
|
87
|
+
* @param handle subprocess handle
|
|
88
|
+
* @param maxResults 候选上限
|
|
89
|
+
* @returns 路径与完整性状态
|
|
90
|
+
*/
|
|
91
|
+
export function parseOutput(handle: SearchHandle, maxResults: number): { files: string[]; truncated: boolean; complete: boolean } {
|
|
92
|
+
const reader = handle.collected?.stdout
|
|
93
|
+
if (!reader) return { files: [], truncated: false, complete: false }
|
|
94
|
+
const output = reader.readFrom(0)
|
|
95
|
+
const values = output.text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
96
|
+
const unique = [...new Set(values)]
|
|
97
|
+
const truncated = Boolean(output.lossy) || unique.length > maxResults
|
|
98
|
+
return { files: unique.slice(0, maxResults).map(pathText), truncated, complete: !truncated }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 使用打包 ripgrep 搜索工作区文件。
|
|
103
|
+
* @author ddj 2026年08月24号
|
|
104
|
+
* @param input provider 输入
|
|
105
|
+
* @returns 有界搜索结果
|
|
106
|
+
*/
|
|
107
|
+
export async function searchRipgrep(input: WorkspaceSearchInput): Promise<WorkspaceSearchResult> {
|
|
108
|
+
const sub = input.ctx.get('subprocess') as SearchSubprocess | undefined
|
|
109
|
+
if (!sub) throw new Error('缺少 subprocess')
|
|
110
|
+
const binary = ripgrepPath()
|
|
111
|
+
if (!binary) throw new Error('ripgrep 不可用')
|
|
112
|
+
const root = input.root ?? await searchRoot(input.ctx, input.session)
|
|
113
|
+
const query = prepareQuery(input.query)
|
|
114
|
+
const argv = [binary, '--no-config', '--files', '--hidden', '--no-ignore', '--glob-case-insensitive', '--glob', queryGlob(query.text)]
|
|
115
|
+
for (const excluded of EXCLUDES) argv.push('--glob', '!**/' + excluded + '/**')
|
|
116
|
+
argv.push('--', root)
|
|
117
|
+
const handle = sub.spawn({
|
|
118
|
+
argv,
|
|
119
|
+
cwd: root,
|
|
120
|
+
stdio: { stdin: 'ignore', stdout: { maxBytes: STDOUT_CAP }, stderr: { maxBytes: STDERR_CAP } },
|
|
121
|
+
graceMs: GRACE_MS,
|
|
122
|
+
signal: input.signal,
|
|
123
|
+
})
|
|
124
|
+
let outcome: { exitCode: number | null; code?: number | null }
|
|
125
|
+
try {
|
|
126
|
+
outcome = await handle.done
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw new Error('ripgrep 启动失败:' + String(error))
|
|
129
|
+
}
|
|
130
|
+
const code = outcome.exitCode ?? outcome.code
|
|
131
|
+
if (code !== 0 && code !== 1) throw new Error('ripgrep 退出码:' + String(code))
|
|
132
|
+
const parsed = parseOutput(handle, input.maxResults)
|
|
133
|
+
if (!handle.collected?.stdout) throw new Error('ripgrep stdout 不可用')
|
|
134
|
+
return { ...parsed, source: 'ripgrep' }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 构造 ripgrep provider。
|
|
139
|
+
* @author ddj 2026年08月24号
|
|
140
|
+
* @returns provider 实例
|
|
141
|
+
*/
|
|
142
|
+
export function newRgProvider(): WorkspaceSearchProvider {
|
|
143
|
+
return { search: searchRipgrep }
|
|
144
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — 文件搜索内部契约。
|
|
3
|
+
* 不暴露给 shared RPC,供 provider、编排和排序层独立演进。
|
|
4
|
+
* 作者 ddj 2026年08月24号
|
|
5
|
+
*/
|
|
6
|
+
import type { Ctx, Session } from '../store.js'
|
|
7
|
+
|
|
8
|
+
export type SearchSource = 'ripgrep' | 'fallback' | 'active-diff'
|
|
9
|
+
|
|
10
|
+
export interface WorkspaceSearchInput {
|
|
11
|
+
ctx: Ctx
|
|
12
|
+
session: Session
|
|
13
|
+
cwd: string
|
|
14
|
+
query: string
|
|
15
|
+
maxResults: number
|
|
16
|
+
signal?: AbortSignal
|
|
17
|
+
root?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface WorkspaceSearchResult {
|
|
21
|
+
files: string[]
|
|
22
|
+
truncated: boolean
|
|
23
|
+
complete: boolean
|
|
24
|
+
source: SearchSource
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface WorkspaceSearchProvider {
|
|
28
|
+
search(input: WorkspaceSearchInput): Promise<WorkspaceSearchResult>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PreparedQuery {
|
|
32
|
+
raw: string
|
|
33
|
+
text: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SearchCandidate {
|
|
37
|
+
path: string
|
|
38
|
+
basename: string
|
|
39
|
+
normalizedPath: string
|
|
40
|
+
source: 'workspace' | 'active-diff' | 'history'
|
|
41
|
+
score?: number
|
|
42
|
+
matchRanges?: Array<{ start: number; end: number }>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CandidateRanker {
|
|
46
|
+
rank(candidates: SearchCandidate[], query: PreparedQuery): SearchCandidate[]
|
|
47
|
+
}
|