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.
@@ -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
+ }