dsh-vscode-mode 0.1.28 → 0.1.29

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,319 @@
1
+ /**
2
+ * dsh-vscode-mode host — 工作区内容搜索(rg --json 主路径 + 有界编排)。
3
+ * 命中列由 rg 行内字节偏移转 UTF-16(1-based),可直接供 Monaco 跳转/高亮。
4
+ * 无 fallback:provider 失败抛错,由 RPC 层转错误响应(避免整树读文件)。
5
+ * 作者 ddj 2026年08月26号
6
+ */
7
+ import type { Ctx, Session } from '../store.js'
8
+ import { pathText } from './query.js'
9
+ import { EXCLUDES, FILE_EXCLUDES, ripgrepPath, searchRoot } from './ripgrep.js'
10
+ import { SearchCache } from './orchestrator.js'
11
+ import type { ContentMatch, ContentSearchInput, ContentSearchProvider, ContentSearchResult } from './types.js'
12
+
13
+ const STDOUT_CAP = 16 << 20
14
+ const STDERR_CAP = 64 << 10
15
+ const GRACE_MS = 20_000
16
+ const DEFAULT_MAX_MATCHES = 500
17
+ const DEFAULT_MAX_FILES = 100
18
+ const CACHE_TTL = 60_000
19
+ const CACHE_LIMIT = 100
20
+ const PROVIDER_VERSION = 'content-ripgrep-v1'
21
+ /** 单行 JSON 记录上限:巨型单行文件(源映射/打包产物)的命中行对搜索 UI 无意义,直接跳过。 */
22
+ const MAX_RECORD_LINE = 1 << 20
23
+
24
+ /** rg --json 单条 match 记录的宽松形状(未知字段忽略)。 */
25
+ interface RgJsonItem {
26
+ type?: string
27
+ data?: {
28
+ path?: { text?: string }
29
+ lines?: { text?: string }
30
+ line_number?: number
31
+ submatches?: Array<{ start?: number; end?: number }>
32
+ }
33
+ }
34
+
35
+ /** 内容搜索请求(RPC 参数映射)。 */
36
+ export interface ContentSearchRequest {
37
+ session: Session
38
+ cwd: string
39
+ query: string
40
+ matchCase?: boolean
41
+ wholeWord?: boolean
42
+ regex?: boolean
43
+ maxResults?: number
44
+ /** 正选 glob(仅在这些文件内搜索)。 */
45
+ include?: string[]
46
+ /** 排除 glob(这些文件不参与搜索)。 */
47
+ exclude?: string[]
48
+ }
49
+
50
+ type ContentSearchResponse = { matches: ContentMatch[]; truncated: boolean }
51
+
52
+ /**
53
+ * 把行内字节偏移转成 UTF-16 列(1-based;越界钳到行尾)。
54
+ * @author ddj 2026年08月26号
55
+ * @param text 行文本
56
+ * @param byteOffset 行内字节偏移(rg submatches.start/end,实测为行相对)
57
+ * @returns 1-based UTF-16 列
58
+ */
59
+ export function byteToUtf16Col(text: string, byteOffset: number): number {
60
+ let bytes = 0
61
+ let col = 1
62
+ for (const char of text) {
63
+ if (bytes >= byteOffset) break
64
+ bytes += Buffer.byteLength(char)
65
+ col++
66
+ }
67
+ return col
68
+ }
69
+
70
+ /**
71
+ * 根内绝对路径 → 工作区相对显示路径(供 edrv.read 与面板展示)。
72
+ * @author ddj 2026年08月26号
73
+ * @param value rg 输出路径
74
+ * @param root 搜索根(subprocess 执行世界)
75
+ * @returns 相对路径(根外路径原样返回)
76
+ */
77
+ export function displayPathOf(value: string | undefined, root: string): string {
78
+ const path = pathText(String(value ?? ''))
79
+ if (!path) return path
80
+ const base = pathText(root).replace(/\/$/, '')
81
+ const lowerPath = path.toLocaleLowerCase('en-US')
82
+ const lowerBase = base.toLocaleLowerCase('en-US')
83
+ if (lowerPath === lowerBase) return '.'
84
+ if (lowerPath.startsWith(lowerBase + '/')) return path.slice(base.length + 1)
85
+ return path
86
+ }
87
+
88
+ /**
89
+ * 解析 rg --json 输出为扁平命中列表(多子匹配展开,列转 UTF-16)。
90
+ * @author ddj 2026年08月26号
91
+ * @param text stdout 文本
92
+ * @param root 搜索根
93
+ * @returns 命中列表
94
+ */
95
+ export function parseRgJsonLines(text: string, root: string): ContentMatch[] {
96
+ const matches: ContentMatch[] = []
97
+ for (const line of text.split(/\r?\n/)) {
98
+ const trimmed = line.trim()
99
+ if (!trimmed) continue
100
+ let item: RgJsonItem
101
+ try {
102
+ item = JSON.parse(trimmed) as RgJsonItem
103
+ } catch {
104
+ continue
105
+ }
106
+ if (item?.type !== 'match' || !item.data) continue
107
+ const data = item.data
108
+ const path = displayPathOf(data.path?.text, root)
109
+ const lineText = (data.lines?.text ?? '').replace(/\r?\n$/, '').replace(/\r$/, '')
110
+ if (lineText.length > MAX_RECORD_LINE) continue
111
+ const lineNumber = data.line_number ?? 0
112
+ for (const sub of data.submatches ?? []) {
113
+ matches.push({
114
+ path,
115
+ line: lineNumber,
116
+ startColumn: byteToUtf16Col(lineText, sub.start ?? 0),
117
+ endColumn: byteToUtf16Col(lineText, sub.end ?? 0),
118
+ text: lineText,
119
+ })
120
+ }
121
+ }
122
+ return matches
123
+ }
124
+
125
+ /**
126
+ * 命中截断:匹配数/文件数双上限,超限标记 truncated。
127
+ * @author ddj 2026年08月26号
128
+ * @param matches 全部命中
129
+ * @param maxMatches 匹配数上限
130
+ * @param maxFiles 文件数上限
131
+ * @returns 截断后的命中与标志
132
+ */
133
+ export function applyCaps(matches: ContentMatch[], maxMatches: number, maxFiles: number): { matches: ContentMatch[]; truncated: boolean } {
134
+ const seen = new Set<string>()
135
+ const out: ContentMatch[] = []
136
+ let truncated = false
137
+ for (const match of matches) {
138
+ if (out.length >= maxMatches) { truncated = true; break }
139
+ if (!seen.has(match.path)) {
140
+ if (seen.size >= maxFiles) { truncated = true; break }
141
+ seen.add(match.path)
142
+ }
143
+ out.push(match)
144
+ }
145
+ return { matches: out, truncated }
146
+ }
147
+
148
+ /**
149
+ * 构造 rg 内容搜索 argv(pattern 放 `--` 后首位置,支持前导 `-` 的查询)。
150
+ * include/exclude 为用户 glob(gitignore 式,原样透传不转义),
151
+ * 分别转正选 `--glob` 与排除 `--glob !`。
152
+ * @author ddj 2026年08月26号
153
+ * @param binary rg 路径
154
+ * @param root 搜索根
155
+ * @param input 搜索输入
156
+ * @returns argv 数组
157
+ */
158
+ export function contentArgv(binary: string, root: string, input: ContentSearchInput): string[] {
159
+ const argv = [binary, '--no-config', '--json', '--line-number', '--no-heading', '--color', 'never', '--hidden', '--no-ignore']
160
+ if (input.matchCase === true) argv.push('--case-sensitive')
161
+ if (input.wholeWord === true) argv.push('--word-regexp')
162
+ if (input.regex !== true) argv.push('--fixed-strings')
163
+ for (const excluded of EXCLUDES) argv.push('--glob', '!**/' + excluded + '/**')
164
+ for (const excluded of FILE_EXCLUDES) argv.push('--glob', '!' + excluded)
165
+ for (const pattern of input.include ?? []) {
166
+ const glob = String(pattern).trim()
167
+ if (glob) argv.push('--glob', glob)
168
+ }
169
+ for (const pattern of input.exclude ?? []) {
170
+ const glob = String(pattern).trim()
171
+ if (glob) argv.push('--glob', '!' + glob)
172
+ }
173
+ argv.push('--', input.query, root)
174
+ return argv
175
+ }
176
+
177
+ /**
178
+ * 使用打包 ripgrep 搜索文件内容。
179
+ * @author ddj 2026年08月26号
180
+ * @param input provider 输入
181
+ * @returns 有界内容搜索结果
182
+ */
183
+ export async function searchRipgrepContent(input: ContentSearchInput): Promise<ContentSearchResult> {
184
+ const sub = input.ctx.get('subprocess') as { spawn(spec: unknown): { done: Promise<{ exitCode: number | null; code?: number | null }>; collected?: { stdout?: { readFrom(offset: number): { text: string; lossy?: boolean } } } } } | undefined
185
+ if (!sub) throw new Error('缺少 subprocess')
186
+ const binary = ripgrepPath()
187
+ if (!binary) throw new Error('ripgrep 不可用')
188
+ const root = input.root ?? await searchRoot(input.ctx, input.session)
189
+ const handle = sub.spawn({
190
+ argv: contentArgv(binary, root, input),
191
+ cwd: root,
192
+ stdio: { stdin: 'ignore', stdout: { maxBytes: STDOUT_CAP }, stderr: { maxBytes: STDERR_CAP } },
193
+ graceMs: GRACE_MS,
194
+ signal: input.signal,
195
+ })
196
+ let outcome: { exitCode: number | null; code?: number | null }
197
+ try {
198
+ outcome = await handle.done
199
+ } catch (error) {
200
+ throw new Error('ripgrep 启动失败:' + String(error))
201
+ }
202
+ const code = outcome.exitCode ?? outcome.code
203
+ if (code !== 0 && code !== 1) throw new Error('ripgrep 退出码:' + String(code))
204
+ const reader = handle.collected?.stdout
205
+ if (!reader) throw new Error('ripgrep stdout 不可用')
206
+ const output = reader.readFrom(0)
207
+ const all = parseRgJsonLines(output.text, root)
208
+ const maxMatches = Math.max(1, Math.min(input.maxResults ?? DEFAULT_MAX_MATCHES, DEFAULT_MAX_MATCHES))
209
+ const capped = applyCaps(all, maxMatches, DEFAULT_MAX_FILES)
210
+ const truncated = Boolean(output.lossy) || capped.truncated
211
+ return { ...capped, complete: !truncated, source: 'ripgrep' }
212
+ }
213
+
214
+ /**
215
+ * 内容搜索编排器:短期缓存 + 同根在途取消 + 会话清理。
216
+ * @author ddj 2026年08月26号
217
+ */
218
+ export class ContentSearcher {
219
+ private readonly cache = new SearchCache<ContentSearchResult>((r) => ({ ...r, matches: [...r.matches] }))
220
+ private readonly inflight = new Map<string, AbortController>()
221
+ private readonly rootsByCwd = new Map<string, Set<string>>()
222
+
223
+ /**
224
+ * 创建内容搜索编排器。
225
+ * @author ddj 2026年08月26号
226
+ * @param ctx DSH 上下文
227
+ * @param provider 内容搜索 provider(测试可替换)
228
+ */
229
+ constructor(private readonly ctx: Ctx, private readonly provider: ContentSearchProvider = { search: searchRipgrepContent }) {}
230
+
231
+ /**
232
+ * 执行一次工作区内容搜索。
233
+ * @author ddj 2026年08月26号
234
+ * @param request 搜索请求
235
+ * @returns RPC 响应字段(provider 失败抛错,abort 返回空)
236
+ */
237
+ async search(request: ContentSearchRequest): Promise<ContentSearchResponse> {
238
+ const query = String(request.query ?? '').trim()
239
+ if (query.length < 2) return { matches: [], truncated: false }
240
+ let root: string
241
+ try {
242
+ root = await searchRoot(this.ctx, request.session)
243
+ } catch {
244
+ return { matches: [], truncated: false }
245
+ }
246
+ const rootKey = pathText(root)
247
+ const roots = this.rootsByCwd.get(request.cwd) ?? new Set<string>()
248
+ roots.add(rootKey)
249
+ this.rootsByCwd.set(request.cwd, roots)
250
+ const includeKey = (request.include ?? []).join(',')
251
+ const excludeKey = (request.exclude ?? []).join(',')
252
+ const key = [rootKey, query, request.matchCase ? 'mc' : '', request.wholeWord ? 'ww' : '', request.regex ? 'rx' : '', includeKey, excludeKey, PROVIDER_VERSION].join('|')
253
+ const cached = this.cache.get(key)
254
+ if (cached) return { matches: cached.matches, truncated: cached.truncated }
255
+ this.inflight.get(root)?.abort()
256
+ const controller = new AbortController()
257
+ this.inflight.set(root, controller)
258
+ try {
259
+ const result = await this.provider.search({
260
+ ctx: this.ctx,
261
+ session: request.session,
262
+ cwd: request.cwd,
263
+ query,
264
+ matchCase: request.matchCase,
265
+ wholeWord: request.wholeWord,
266
+ regex: request.regex,
267
+ maxResults: request.maxResults,
268
+ include: request.include,
269
+ exclude: request.exclude,
270
+ signal: controller.signal,
271
+ root,
272
+ })
273
+ if (controller.signal.aborted) return { matches: [], truncated: false }
274
+ this.cache.set(key, result)
275
+ return { matches: result.matches, truncated: result.truncated }
276
+ } catch (error) {
277
+ if (controller.signal.aborted) return { matches: [], truncated: false }
278
+ throw error
279
+ } finally {
280
+ if (this.inflight.get(root) === controller) this.inflight.delete(root)
281
+ }
282
+ }
283
+
284
+ /**
285
+ * 清理会话对应根目录的缓存和在途搜索。
286
+ * @author ddj 2026年08月26号
287
+ * @param cwd 会话工作区
288
+ */
289
+ dispose(cwd: string): void {
290
+ const roots = this.rootsByCwd.get(cwd) ?? new Set<string>([cwd])
291
+ for (const root of roots) {
292
+ this.inflight.get(root)?.abort()
293
+ this.inflight.delete(root)
294
+ this.cache.clearRoot(root)
295
+ }
296
+ this.rootsByCwd.delete(cwd)
297
+ }
298
+
299
+ /**
300
+ * 清理全部状态。
301
+ * @author ddj 2026年08月26号
302
+ */
303
+ disposeAll(): void {
304
+ for (const controller of this.inflight.values()) controller.abort()
305
+ this.inflight.clear()
306
+ this.rootsByCwd.clear()
307
+ this.cache.clear()
308
+ }
309
+ }
310
+
311
+ /**
312
+ * 创建默认内容搜索编排器。
313
+ * @author ddj 2026年08月26号
314
+ * @param ctx DSH 上下文
315
+ * @returns 内容搜索编排器
316
+ */
317
+ export function newContentSearcher(ctx: Ctx): ContentSearcher {
318
+ return new ContentSearcher(ctx)
319
+ }
@@ -17,7 +17,7 @@ interface SearchFs {
17
17
  }
18
18
 
19
19
  const RESULT_CAP = 500
20
- const EXCLUDED = new Set(['node_modules', '.git', '.tmp', '.cache', 'dist', 'build', 'vendor', 'coverage', '__pycache__'])
20
+ const EXCLUDED = new Set(['node_modules', '.git', '.tmp', '.cache', 'dist', 'build', 'vendor', 'coverage', '__pycache__', '.pnpm-store', '.npm-cache', '.codegraph', '.workbuddy', '.dsh-edit-review.json', '.dsh-edit-review-archive.json', '.dsh-edit-review-debug.log'])
21
21
 
22
22
  /**
23
23
  * 在一个目录树中递归查找匹配文件。
@@ -18,17 +18,24 @@ const RESULT_LIMIT = 50
18
18
  const PROVIDER_VERSION = 'ripgrep-v1'
19
19
  const POLICY_VERSION = 'search-policy-v1'
20
20
 
21
- type CacheEntry = { at: number; result: WorkspaceSearchResult }
21
+ type CacheEntry<T> = { at: number; result: T }
22
22
  type SearchRequest = { session: Session; cwd: string; query: string; activePaths: string[] }
23
23
  type SearchResponse = { files: string[]; truncated: boolean }
24
24
 
25
25
  /**
26
- * 短期有界搜索缓存。
26
+ * 短期有界搜索缓存(泛型:文件/内容搜索共用;clone 由调用方保证写隔离)。
27
27
  * @author ddj 2026年08月24号
28
- * @returns 缓存对象
28
+ * @param clone 结果写隔离克隆(缺省恒等)
29
29
  */
30
- export class SearchCache {
31
- private readonly entries = new Map<string, CacheEntry>()
30
+ export class SearchCache<T> {
31
+ private readonly entries = new Map<string, CacheEntry<T>>()
32
+
33
+ /**
34
+ * 创建缓存。
35
+ * @author ddj 2026年08月24号
36
+ * @param clone 结果克隆函数(缺省直接引用)
37
+ */
38
+ constructor(private readonly clone: (value: T) => T = (value) => value) {}
32
39
 
33
40
  /**
34
41
  * 读取未过期条目。
@@ -37,13 +44,13 @@ export class SearchCache {
37
44
  * @param now 当前时间
38
45
  * @returns 缓存结果或 undefined
39
46
  */
40
- get(key: string, now = Date.now()): WorkspaceSearchResult | undefined {
47
+ get(key: string, now = Date.now()): T | undefined {
41
48
  const entry = this.entries.get(key)
42
49
  if (!entry) return undefined
43
50
  if (now - entry.at >= CACHE_TTL) { this.entries.delete(key); return undefined }
44
51
  this.entries.delete(key)
45
52
  this.entries.set(key, entry)
46
- return { ...entry.result, files: [...entry.result.files] }
53
+ return this.clone(entry.result)
47
54
  }
48
55
 
49
56
  /**
@@ -53,9 +60,9 @@ export class SearchCache {
53
60
  * @param result provider 结果
54
61
  * @param now 当前时间
55
62
  */
56
- set(key: string, result: WorkspaceSearchResult, now = Date.now()): void {
63
+ set(key: string, result: T, now = Date.now()): void {
57
64
  this.entries.delete(key)
58
- this.entries.set(key, { at: now, result: { ...result, files: [...result.files] } })
65
+ this.entries.set(key, { at: now, result: this.clone(result) })
59
66
  while (this.entries.size > CACHE_LIMIT) this.entries.delete(this.entries.keys().next().value as string)
60
67
  }
61
68
 
@@ -145,7 +152,7 @@ function mergeCandidates(result: WorkspaceSearchResult, activePaths: string[], r
145
152
  * @author ddj 2026年08月24号
146
153
  */
147
154
  export class SearchOrchestrator {
148
- private readonly cache = new SearchCache()
155
+ private readonly cache = new SearchCache<WorkspaceSearchResult>((r) => ({ ...r, files: [...r.files] }))
149
156
  private readonly inflight = new Map<string, AbortController>()
150
157
  private readonly rootsByCwd = new Map<string, Set<string>>()
151
158
  private readonly provider: WorkspaceSearchProvider
@@ -14,9 +14,23 @@ import type { WorkspaceSearchInput, WorkspaceSearchProvider, WorkspaceSearchResu
14
14
  const STDOUT_CAP = 4 << 20
15
15
  const STDERR_CAP = 64 << 10
16
16
  const GRACE_MS = 20_000
17
- const EXCLUDES = [
17
+
18
+ /** 默认排除目录(文件/内容搜索共用,导出供 provider 复用)。 */
19
+ export const EXCLUDES = [
18
20
  'node_modules', '.git', '.tmp', '.cache', 'dist', 'build', 'vendor',
19
- 'coverage', '__pycache__',
21
+ 'coverage', '__pycache__', '.pnpm-store', '.npm-cache', '.codegraph', '.workbuddy',
22
+ ]
23
+
24
+ /**
25
+ * 默认排除文件(glob 模式,文件/内容搜索共用)。
26
+ * 插件自身 sidecar(内容搜索会命中其历史编辑文本,且单行巨型 JSON 会撑爆输出上限);
27
+ * *.map 源映射(纯构建产物,内容搜索无意义且常为单行巨文件)。
28
+ */
29
+ export const FILE_EXCLUDES = [
30
+ '**/.dsh-edit-review.json',
31
+ '**/.dsh-edit-review-archive.json',
32
+ '**/.dsh-edit-review-debug.log',
33
+ '**/*.map',
20
34
  ]
21
35
 
22
36
  interface SearchHandle {
@@ -113,6 +127,7 @@ export async function searchRipgrep(input: WorkspaceSearchInput): Promise<Worksp
113
127
  const query = prepareQuery(input.query)
114
128
  const argv = [binary, '--no-config', '--files', '--hidden', '--no-ignore', '--glob-case-insensitive', '--glob', queryGlob(query.text)]
115
129
  for (const excluded of EXCLUDES) argv.push('--glob', '!**/' + excluded + '/**')
130
+ for (const excluded of FILE_EXCLUDES) argv.push('--glob', '!' + excluded)
116
131
  argv.push('--', root)
117
132
  const handle = sub.spawn({
118
133
  argv,
@@ -45,3 +45,43 @@ export interface SearchCandidate {
45
45
  export interface CandidateRanker {
46
46
  rank(candidates: SearchCandidate[], query: PreparedQuery): SearchCandidate[]
47
47
  }
48
+
49
+ /** 内容搜索单条命中(列 = 1-based UTF-16,直接供 Monaco 跳转/高亮)。 */
50
+ export interface ContentMatch {
51
+ path: string
52
+ line: number
53
+ startColumn: number
54
+ endColumn: number
55
+ text: string
56
+ }
57
+
58
+ /** 内容搜索输入(options 缺省 = 大小写不敏感、字面匹配、非全词;include/exclude 为 rg glob)。 */
59
+ export interface ContentSearchInput {
60
+ ctx: Ctx
61
+ session: Session
62
+ cwd: string
63
+ query: string
64
+ matchCase?: boolean
65
+ wholeWord?: boolean
66
+ regex?: boolean
67
+ maxResults?: number
68
+ /** 正选 glob(仅在这些文件内搜索;逗号已在客户端拆分)。 */
69
+ include?: string[]
70
+ /** 排除 glob(这些文件不参与搜索)。 */
71
+ exclude?: string[]
72
+ signal?: AbortSignal
73
+ root?: string
74
+ }
75
+
76
+ /** 内容搜索输出:扁平命中列表 + 截断标志。 */
77
+ export interface ContentSearchResult {
78
+ matches: ContentMatch[]
79
+ truncated: boolean
80
+ complete: boolean
81
+ source: SearchSource
82
+ }
83
+
84
+ /** 内容搜索 provider(rg JSON 主路径;无 fallback,失败抛错由调用方处理)。 */
85
+ export interface ContentSearchProvider {
86
+ search(input: ContentSearchInput): Promise<ContentSearchResult>
87
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * dsh-vscode-mode shared — 快捷键命令目录与默认键位(双面契约)。
3
+ * 纯数据模块:host(settings schema 默认值)与 client(执行匹配/设置页)共用。
4
+ * 键位格式:修饰符 + 主键,`+` 连接,如 `Ctrl+Shift+F`;空串 = 未绑定。
5
+ * 作者 ddj 2026年08月26号
6
+ */
7
+
8
+ /** 命令 id → 默认键位。命令目录以此为准,新增命令只需加一项。 */
9
+ export const KEYBINDING_DEFAULTS: Record<string, string> = {
10
+ 'edrv.save': 'Ctrl+S',
11
+ 'edrv.quickOpen': 'Ctrl+P',
12
+ 'edrv.toggleSidebar': 'Ctrl+B',
13
+ 'edrv.searchInFiles': 'Ctrl+Shift+F',
14
+ }
15
+
16
+ /**
17
+ * 返回默认键位的独立副本(调用方修改不污染目录)。
18
+ * @author ddj 2026年08月26号
19
+ * @returns 默认键位映射
20
+ */
21
+ export function defaultKeybindings(): Record<string, string> {
22
+ return { ...KEYBINDING_DEFAULTS }
23
+ }
24
+
25
+ /**
26
+ * 规整外部键位数据:只保留已知命令 id 且为字符串的值,未知 id 丢弃。
27
+ * 防止用户文档/旧版本写入的未知命令污染执行匹配。
28
+ * @author ddj 2026年08月26号
29
+ * @param raw 原始设置值
30
+ * @returns 规整后的键位映射(缺省项不补默认值,由调用方按需合并)
31
+ */
32
+ export function normalizeKeybindings(raw: unknown): Record<string, string> {
33
+ const out: Record<string, string> = {}
34
+ if (!raw || typeof raw !== 'object') return out
35
+ for (const [id, chord] of Object.entries(raw as Record<string, unknown>)) {
36
+ if (id in KEYBINDING_DEFAULTS && typeof chord === 'string') out[id] = chord
37
+ }
38
+ return out
39
+ }
package/src/shared/rpc.ts CHANGED
@@ -29,6 +29,15 @@ export interface TreeEntry {
29
29
  size?: number
30
30
  }
31
31
 
32
+ /** 内容搜索单条命中(edrv.searchContent 返回;path 相对工作区根,列为 1-based UTF-16)。 */
33
+ export interface SearchContentMatch {
34
+ path: string
35
+ line: number
36
+ startColumn: number
37
+ endColumn: number
38
+ text: string
39
+ }
40
+
32
41
  /** 一条批量决策项(decideBatch 的 items 元素,与 accept/reject 单条参数同构)。 */
33
42
  export interface DecideItem {
34
43
  callId: string
@@ -59,6 +68,7 @@ export interface RpcRequestMap {
59
68
  'edrv.rollback': { sessionId?: string; path: string; batch?: number }
60
69
  'edrv.debug': { sessionId?: string; text: string }
61
70
  'edrv.searchFiles': { sessionId?: string; query: string }
71
+ 'edrv.searchContent': { sessionId?: string; query: string; matchCase?: boolean; wholeWord?: boolean; regex?: boolean; maxResults?: number; include?: string[]; exclude?: string[] }
62
72
  'edrv.listDir': { sessionId?: string; path: string }
63
73
  'edrv.revealInExplorer': { sessionId?: string; path: string }
64
74
  'mcp.list': {}
@@ -94,6 +104,7 @@ export interface RpcOkMap {
94
104
  'edrv.rollback': { path: string; batch: number | null }
95
105
  'edrv.debug': object
96
106
  'edrv.searchFiles': { files: string[]; truncated: boolean }
107
+ 'edrv.searchContent': { matches: SearchContentMatch[]; truncated: boolean }
97
108
  'edrv.listDir': { root: string; path: string; entries: TreeEntry[] }
98
109
  'edrv.revealInExplorer': { revealed: string }
99
110
  'mcp.list': { servers: MpcServer[] }