dsh-vscode-mode 0.6.0 → 0.8.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.
Files changed (57) hide show
  1. package/README.md +40 -6
  2. package/assets/icon.svg +7 -0
  3. package/cordis.patch.yml +12 -0
  4. package/lib/client.js +9317 -7494
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +1295 -173
  7. package/lib/index.js.map +1 -1
  8. package/locale/en.json +4 -0
  9. package/locale/zh.json +4 -0
  10. package/package.json +3 -1
  11. package/src/client/compat.ts +134 -4
  12. package/src/client/copyText.ts +28 -0
  13. package/src/client/fileClipboard.ts +46 -0
  14. package/src/client/imagePreview.ts +76 -2
  15. package/src/client/index.ts +68 -39
  16. package/src/client/monaco/lsp/lspClient.ts +46 -0
  17. package/src/client/monaco/lsp/providers.ts +245 -1
  18. package/src/client/nativeOpenStore.ts +91 -0
  19. package/src/client/officialSidebar.ts +11 -45
  20. package/src/client/openWithDialog.ts +66 -0
  21. package/src/client/outline/index.ts +1 -1
  22. package/src/client/pdf/pdfPanel.ts +18 -0
  23. package/src/client/searchSeed.ts +36 -0
  24. package/src/client/settingsContext.ts +6 -4
  25. package/src/client/sidebar/contextMenu.ts +51 -10
  26. package/src/client/sidebar/menuItems.ts +473 -50
  27. package/src/client/sidebar/panels/FileExplorer.ts +1 -1
  28. package/src/client/sidebar/panels/SearchPanel.ts +24 -3
  29. package/src/client/sidebar/panels/SvnPanel.ts +1 -1
  30. package/src/client/sidebar/panels/index.ts +1 -1
  31. package/src/client/sidebar/types.ts +6 -0
  32. package/src/client/styles/editor.css +5 -3
  33. package/src/client/svnStore.ts +6 -1
  34. package/src/client/tabActions.ts +22 -10
  35. package/src/client/ui/EditorView.ts +198 -25
  36. package/src/client/ui/McpSettings.ts +30 -0
  37. package/src/client/ui/OfficialSideTab.ts +1 -0
  38. package/src/client/ui/PerfSettings.ts +4 -1
  39. package/src/client/ui/PromptDialog.ts +63 -0
  40. package/src/client/ui/SideEditorTab.ts +1 -0
  41. package/src/client/ui/commandCatalog.ts +14 -0
  42. package/src/client/ui/icons.ts +72 -0
  43. package/src/compat.ts +2 -2
  44. package/src/dshVersion.ts +2 -1
  45. package/src/fileOpenSettings.ts +163 -40
  46. package/src/index.ts +15 -1
  47. package/src/lsp/client.ts +24 -0
  48. package/src/lsp/rpc.ts +57 -0
  49. package/src/lsp/server.ts +272 -2
  50. package/src/perf.ts +90 -4
  51. package/src/routes.ts +13 -1
  52. package/src/rpc.ts +294 -36
  53. package/src/shared/fsNames.ts +115 -0
  54. package/src/shared/keybindings.ts +2 -0
  55. package/src/shared/lsp.ts +79 -0
  56. package/src/shared/nativeOpen.ts +91 -0
  57. package/src/shared/rpc.ts +158 -1
@@ -10,12 +10,34 @@
10
10
  import {
11
11
  pathOfModel, syncDoc, closeDoc, findDefinition, findReferences,
12
12
  fetchDocumentSymbols, fetchHover, fetchSemanticTokens,
13
+ fetchCompletions, resolveCompletion, fetchSignatureHelp,
13
14
  targetOpenPath, lspStatusFor, refreshStatus,
14
15
  lspUriToAbs,
15
16
  } from './lspClient.js'
16
- import { LSP_SEMANTIC_TOKEN_MODIFIERS, LSP_SEMANTIC_TOKEN_TYPES } from '../../../shared/lsp.js'
17
+ import {
18
+ LSP_SEMANTIC_TOKEN_MODIFIERS, LSP_SEMANTIC_TOKEN_TYPES,
19
+ LSP_COMPLETION_KIND_NAMES, LSP_INSERT_TEXT_FORMAT_SNIPPET,
20
+ } from '../../../shared/lsp.js'
17
21
 
18
22
  const LSP_LANGS = ['lua', 'csharp']
23
+
24
+ /**
25
+ * 补全触发字符(按语言)。
26
+ *
27
+ * 取 EmmyLua / DotRush 声明字符的**交集**且去掉噪声项:`"` `'` `\` `/` `#` 等在多语言里
28
+ * 过于聒噪(每次输入都弹列表打扰手写),故只保留「成员访问 + 调用 + 索引」三类主场景。
29
+ * @author ddj 2026年09月22号
30
+ */
31
+ const COMPLETION_TRIGGERS = {
32
+ lua: ['.', ':', '(', '['],
33
+ csharp: ['.', ':', '(', '[', '<', '@'],
34
+ }
35
+
36
+ /** 签名帮助触发字符(与 LSP 服务器声明的 triggerCharacters 对齐)。 */
37
+ const SIGNATURE_TRIGGERS = ['(', ',']
38
+
39
+ /** 签名参数/签名文档上浮窗渲染的兜底最大长度(防超长 doc 撑爆浮窗)。 */
40
+ const MAX_DOC_LENGTH = 4000
19
41
  const SEMANTIC_LEGEND = {
20
42
  tokenTypes: [...LSP_SEMANTIC_TOKEN_TYPES],
21
43
  tokenModifiers: [...LSP_SEMANTIC_TOKEN_MODIFIERS],
@@ -100,6 +122,19 @@ export function registerLspProviders(monaco) {
100
122
  disposables.push(() => window.removeEventListener('edrv:lsp-redetect', onRedetect))
101
123
 
102
124
  // —— 数据 provider ——
125
+ // 补全/签名帮助是本轮新增能力,注册前各自探能力:vendored Monaco 若缺某一 API
126
+ // (换了构建/被裁剪),只跳过该 provider,不能让整套 LSP(跳转/hover/语义高亮)一起挂掉。
127
+ // ⚠️ 必须**逐个压入**注销器:按语言注册会返回数组,整组压入会让 disposeLspProviders
128
+ // 既不是 function 也没有 .dispose → 静默跳过不注销 → 重载后补全 provider 叠加(重复候补)。
129
+ const withCapability = (register) => {
130
+ try {
131
+ const disposer = register()
132
+ if (Array.isArray(disposer)) disposables.push(...disposer.filter(Boolean))
133
+ else if (disposer) disposables.push(disposer)
134
+ } catch (error) {
135
+ /* 单个能力注册失败不影响其余 provider */
136
+ }
137
+ }
103
138
  disposables.push(
104
139
  // 原生跳转(peek 参考文献列表点击 / 原生 go to definition 等)的 edrv:// 打开兜底:
105
140
  // 目标 uri 由本插件自己定义(edrv:// 工作区相对路径),Monaco 无法自行加载,
@@ -163,6 +198,33 @@ export function registerLspProviders(monaco) {
163
198
  releaseDocumentSemanticTokens: () => {},
164
199
  }),
165
200
  )
201
+
202
+ // 新增能力走 withCapability(能力缺失只跳过自身,不影响上面已注册的 provider),
203
+ // 且必须在 disposables.push(...) **之外**调用 —— 放进参数列表会压入 undefined 占位。
204
+ // 补全:`obj.` 弹出成员列表(EmmyLua 经 ---@class/---@field 注释索引提供字段)。
205
+ // 逐语言注册以分别给触发字符;resolveCompletionItem 惰性取文档(列表阶段不带)。
206
+ withCapability(() => LSP_LANGS.map((lang) => completionsFor(monaco, lang)))
207
+ withCapability(() => monaco.languages.registerSignatureHelpProvider(LSP_LANGS, {
208
+ signatureHelpTriggerCharacters: SIGNATURE_TRIGGERS,
209
+ signatureHelpRetriggerCharacters: SIGNATURE_TRIGGERS,
210
+ provideSignatureHelp: async (model, position, token, context) => {
211
+ const path = pathOfModel(model)
212
+ if (!path) return null
213
+ // 取消(继续输入/移光标)时不再弹,避免过期签名覆盖新位置
214
+ if (token?.isCancellationRequested) return null
215
+ const help = await fetchSignatureHelp(path, model.getValue(), position)
216
+ if (!help || !help.signatures.length) return null
217
+ return {
218
+ value: {
219
+ signatures: help.signatures.map(toMonacoSignature),
220
+ activeSignature: help.activeSignature,
221
+ activeParameter: help.activeParameter,
222
+ },
223
+ dispose: () => {},
224
+ }
225
+ },
226
+ }))
227
+
166
228
  // 注销器落 window:window.monaco 跨插件重载存活,模块级 registered 会复位,
167
229
  // 只判 registered 会在重载后重复注册全部 LSP provider(见 disposeLspProviders)。
168
230
  if (host) host[LSP_PROVIDERS_GLOBAL] = true
@@ -273,6 +335,188 @@ export async function runFindReferences(ed) {
273
335
  */
274
336
  export function hideReferencesOverlay() {}
275
337
 
338
+ /**
339
+ * 构造某语言的补全 provider(触发字符按语言区分)。
340
+ *
341
+ * 为什么有 documentPath 传递:Monaco 调 `resolveCompletionItem(item, token)` 时**不传 model**
342
+ * (实测 vendored 0.42 构建:`provider.resolveCompletionItem(this.completion, token)`),
343
+ * 因此路径必须在列表阶段随候选项带上,否则 resolve 阶段无处得知该查哪个文档。
344
+ * @author ddj 2026年09月22号
345
+ * @param monaco Monaco 实例
346
+ * @param lang 语言 id
347
+ * @returns provider 注销器
348
+ */
349
+ function completionsFor(monaco, lang) {
350
+ return monaco.languages.registerCompletionItemProvider(lang, {
351
+ triggerCharacters: COMPLETION_TRIGGERS[lang] ?? ['.', ':'],
352
+ provideCompletionItems: async (model, position, context, token) => {
353
+ const path = pathOfModel(model)
354
+ if (!path) return { suggestions: [] }
355
+ const list = await fetchCompletions(path, model.getValue(), position, toLspContext(context))
356
+ if (!list || !list.items.length || token?.isCancellationRequested) return { suggestions: [] }
357
+ const word = model.getWordUntilPosition(position)
358
+ const fallback = {
359
+ startLineNumber: position.lineNumber,
360
+ endLineNumber: position.lineNumber,
361
+ startColumn: word.startColumn,
362
+ endColumn: word.endColumn,
363
+ }
364
+ return {
365
+ suggestions: list.items.map((item) => toMonoSuggestion(monaco, item, fallback, path)),
366
+ incomplete: list.incomplete === true,
367
+ }
368
+ },
369
+ resolveCompletionItem: async (item, token) => {
370
+ const raw = item?.__edrvRaw
371
+ const path = item?.__edrvPath
372
+ if (!raw || !path || token?.isCancellationRequested) return item
373
+ const resolved = await resolveCompletion(path, raw).catch(() => null)
374
+ if (!resolved) return item
375
+ return mergeResolved(monaco, item, resolved)
376
+ },
377
+ })
378
+ }
379
+
380
+ /**
381
+ * Monaco 补全上下文 → LSP CompletionContext。
382
+ *
383
+ * ⚠️ 两侧 triggerKind **编号不同,必须换算**(实测 vendored 0.42 与 LSP 3.17 规范):
384
+ * Monaco:0=Invoke, 1=TriggerCharacter, 2=TriggerForIncompleteCompletions
385
+ * LSP :1=Invoked, 2=TriggerCharacter, 3=TriggerForIncomplete
386
+ * 恒差 1。直传会把「按字符触发」谎报成 Invoked,服务器据此可能不返回成员补全
387
+ * (部分服务器严格按 triggerKind 决定是否给 table 成员)。
388
+ * @author ddj 2026年09月22号
389
+ * @param context Monaco 上下文(可能缺字段)
390
+ * @returns LSP 上下文;无触发信息时 undefined(不占载荷)
391
+ */
392
+ export function toLspContext(context) {
393
+ const kind = context?.triggerKind
394
+ if (typeof kind !== 'number' && !context?.triggerCharacter) return undefined
395
+ // 换算到 LSP 编号空间并夹到合法区间 [1,3];未给 kind 时按 LSP Invoked(1) 处理
396
+ const lspKind = typeof kind === 'number' ? Math.min(3, Math.max(1, kind + 1)) : 1
397
+ const out = { triggerKind: lspKind }
398
+ if (typeof context?.triggerCharacter === 'string') out.triggerCharacter = context.triggerCharacter
399
+ return out
400
+ }
401
+
402
+ /**
403
+ * LSP 补全项 → Monaco suggestion。
404
+ * @author ddj 2026年09月22号
405
+ * @param monaco Monaco 实例(取枚举)
406
+ * @param item host 归一化补全项
407
+ * @param fallback 无 textEdit 时的词范围
408
+ * @param path 工作区相对路径(resolve 阶段回传用)
409
+ * @returns Monaco suggestion
410
+ */
411
+ function toMonoSuggestion(monaco, item, fallback, path) {
412
+ const kindName = LSP_COMPLETION_KIND_NAMES[item.kind ?? 0]
413
+ const kinds = monaco.languages.CompletionItemKind ?? {}
414
+ const asSnippet = monaco.languages.CompletionItemInsertTextRule?.InsertAsSnippet
415
+ const suggestion = {
416
+ label: item.label,
417
+ kind: kindName && typeof kinds[kindName] === 'number' ? kinds[kindName] : (kinds.Text ?? 0),
418
+ insertText: textOfCompletion(item),
419
+ range: rangeOfCompletion(monaco, item, fallback),
420
+ // 非协议字段:仅本插件读取(resolve 需要文档路径与原始条目),Monaco 忽略
421
+ __edrvRaw: item,
422
+ __edrvPath: path,
423
+ }
424
+ if (item.detail) suggestion.detail = item.detail
425
+ if (item.documentation) suggestion.documentation = { value: clip(item.documentation) }
426
+ if (item.filterText) suggestion.filterText = item.filterText
427
+ if (item.sortText) suggestion.sortText = item.sortText
428
+ if (item.preselect === true) suggestion.preselect = true
429
+ if (item.commitCharacters) suggestion.commitCharacters = item.commitCharacters
430
+ if (item.deprecated === true) suggestion.tags = [monaco.languages.CompletionItemTag?.Deprecated ?? 1]
431
+ if (item.additionalTextEdits) suggestion.additionalTextEdits = item.additionalTextEdits.map((edit) => toMonoEdit(monaco, edit))
432
+ // 片段展开:LSP insertTextFormat=2 交给 Monaco 解析 ${1:占位} 语法
433
+ if (item.insertTextFormat === LSP_INSERT_TEXT_FORMAT_SNIPPET && typeof asSnippet === 'number') {
434
+ suggestion.insertTextRules = asSnippet
435
+ }
436
+ return suggestion
437
+ }
438
+
439
+ /**
440
+ * 取补全插入文本:`textEdit.newText` 优先于 `insertText`(协议规定 textEdit 生效时以它为准)。
441
+ * @author ddj 2026年09月22号
442
+ */
443
+ function textOfCompletion(item) {
444
+ if (item.textEdit?.newText) return item.textEdit.newText
445
+ return item.insertText ?? item.label
446
+ }
447
+
448
+ /**
449
+ * 取补全替换范围(Monaco 1-based):单 range 或 insert/replace 双 range 两种形态都映射。
450
+ * 无 textEdit 时回落词范围 —— 缺范围会让 Monaco 用默认范围替换,可能吃掉已输入前缀。
451
+ * @author ddj 2026年09月22号
452
+ */
453
+ function rangeOfCompletion(monaco, item, fallback) {
454
+ const edit = item.textEdit
455
+ if (!edit) return fallback
456
+ if (edit.range) return toMonoRange(monaco, edit.range)
457
+ if (edit.insert && edit.replace) {
458
+ return { insert: toMonoRange(monaco, edit.insert), replace: toMonoRange(monaco, edit.replace) }
459
+ }
460
+ return fallback
461
+ }
462
+
463
+ /** LSP Range(0-based)→ Monaco Range(1-based)。 */
464
+ function toMonoRange(monaco, range) {
465
+ const s = (p) => ({ lineNumber: Math.max(1, (p?.line ?? 0) + 1), column: Math.max(1, (p?.character ?? 0) + 1) })
466
+ const start = s(range?.start)
467
+ const end = s(range?.end)
468
+ return new monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column)
469
+ }
470
+
471
+ /** LSP TextEdit → Monaco IEditOperation(additionalTextEdits 用)。 */
472
+ function toMonoEdit(monaco, edit) {
473
+ return { range: toMonoRange(monaco, edit.range), text: edit.newText }
474
+ }
475
+
476
+ /**
477
+ * 把 resolve 结果合并进已展示的候选项(保留列表阶段的 range/kind 等 Monaco 侧字段)。
478
+ * @author ddj 2026年09月22号
479
+ * @param monaco Monaco 实例
480
+ * @param suggestion 原候选项
481
+ * @param resolved resolve 后的归一化条目
482
+ * @returns 合并后的候选项
483
+ */
484
+ function mergeResolved(monaco, suggestion, resolved) {
485
+ const merged = { ...suggestion, __edrvRaw: resolved }
486
+ if (resolved.detail) merged.detail = resolved.detail
487
+ if (resolved.documentation) merged.documentation = { value: clip(resolved.documentation) }
488
+ if (resolved.additionalTextEdits) merged.additionalTextEdits = resolved.additionalTextEdits.map((edit) => toMonoEdit(monaco, edit))
489
+ return merged
490
+ }
491
+
492
+ /** 文档裁剪:超长 doc(少数服务器返回整段源码)会撑爆浮窗,截断并标注。 */
493
+ function clip(text) {
494
+ const value = String(text ?? '')
495
+ if (value.length <= MAX_DOC_LENGTH) return value
496
+ return value.slice(0, MAX_DOC_LENGTH) + '\n\n…(内容过长已截断)'
497
+ }
498
+
499
+ /**
500
+ * LSP 签名 → Monaco SignatureInformation。
501
+ * 参数 label 的 `[start, end]` 元组形态**必须**转成 `[start, end]` 数组,
502
+ * 否则 Monaco 按字符串渲染成 `42,57` 这种字面文本(该浮窗会显示成乱码式参数)。
503
+ * @author ddj 2026年09月22号
504
+ * @param signature host 归一化签名
505
+ * @returns Monaco 签名
506
+ */
507
+ function toMonoSignature(signature) {
508
+ const out = {
509
+ label: signature.label,
510
+ parameters: (signature.parameters ?? []).map((param) => {
511
+ const info = { label: Array.isArray(param.label) ? [param.label[0], param.label[1]] : param.label }
512
+ if (param.documentation) info.documentation = { value: clip(param.documentation) }
513
+ return info
514
+ }),
515
+ }
516
+ if (signature.documentation) out.documentation = { value: clip(signature.documentation) }
517
+ return out
518
+ }
519
+
276
520
  /** 编辑器状态栏提示(复用 EditorView 的 status 通道)。 */
277
521
  function setStatus(text) {
278
522
  window.dispatchEvent(new CustomEvent('edrv:status', { detail: { text } }))
@@ -0,0 +1,91 @@
1
+ /**
2
+ * dsh-vscode-mode client — 原生打开范围的运行时状态(设置值缓存 + 官方打开 handler)。
3
+ * 职责:
4
+ * 1. 缓存设置字段 `nativeOpenExts` 的解析结果(mountSettingsSyncs 的 keybindings 同步
5
+ * 块每拍推送;旧版设置无该字段 → 保持默认集,与 deferToOfficial 同源 shared 事实源);
6
+ * 2. 持有「原生打开」执行 handler(index.ts 装配时注册 sidebarRight.openResource 包装),
7
+ * EditorView 的 openFile 链路命中判定时调用,失败回退现状打开。
8
+ * 单例模块级状态:值变更经 CustomEvent 广播(跨组件按需订阅;消费方多为每调用读缓存)。
9
+ * @author ddj 2026年09月22号
10
+ */
11
+ import { DEFAULT_NATIVE_CSV, DEFAULT_NATIVE_EXT, inNativeOpen, parseExtCsv } from '../shared/nativeOpen.js'
12
+
13
+ /** 设置值变化广播事件名(mountSettingsSyncs 推送时派发;幂等值不派发)。 */
14
+ export const NATIVE_OPEN_EVENT = 'edrv:native-open-exts-change'
15
+
16
+ /** 当前设置串(设置页回显 / 诊断用)。 */
17
+ let csv = DEFAULT_NATIVE_CSV
18
+ /** 解析后的后缀集合(热路径读取,避免每次点击 split)。 */
19
+ let exts: ReadonlySet<string> = new Set(DEFAULT_NATIVE_EXT)
20
+ /** 官方原生打开 handler(index.ts 装配注册;返回 false/缺失 = 回退现状打开)。 */
21
+ let handler: ((path: string) => boolean) | null = null
22
+
23
+ /**
24
+ * 读当前原生打开设置串。
25
+ * @author ddj 2026年09月22号
26
+ * @returns 逗号分隔后缀串
27
+ */
28
+ export function nativeCsv(): string {
29
+ return csv
30
+ }
31
+
32
+ /**
33
+ * 该路径是否命中用户配置的原生打开范围(不含 deferToOfficial 默认让位语义——
34
+ * 那部分由调用方自行取并,保持两判定各司其职)。
35
+ * @author ddj 2026年09月22号
36
+ * @param path 文件路径
37
+ * @returns 是否命中
38
+ */
39
+ export function inNativePath(path: unknown): boolean {
40
+ return inNativeOpen(path, exts)
41
+ }
42
+
43
+ /**
44
+ * 推送设置值(mountSettingsSyncs 每拍调用):解析为集合,值变化才更新并广播。
45
+ * 非法/空输入 → 回落默认集(与 host schema default 同源)。
46
+ * @author ddj 2026年09月22号
47
+ * @param value 设置原值(unknown,兼容旧版 undefined)
48
+ */
49
+ export function setNativeCsv(value: unknown): void {
50
+ const next = typeof value === 'string' && value.trim() ? value : DEFAULT_NATIVE_CSV
51
+ if (next === csv) return
52
+ csv = next
53
+ exts = parseExtCsv(next)
54
+ if (exts.size === 0) exts = new Set(DEFAULT_NATIVE_EXT)
55
+ try {
56
+ window.dispatchEvent(new CustomEvent(NATIVE_OPEN_EVENT, { detail: { value: csv } }))
57
+ } catch {
58
+ /* 无 window 环境(测试):仅内存更新 */
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 注册原生打开执行 handler(index.ts 在官方 sidebarRight.openResource 可用时注册)。
64
+ * @author ddj 2026年09月22号
65
+ * @param fn handler(返回是否已原生打开成功);null 解除
66
+ */
67
+ export function setNativeOpenHandler(fn: ((path: string) => boolean) | null): void {
68
+ handler = fn
69
+ }
70
+
71
+ /**
72
+ * 尝试原生打开(EditorView openFile 链路命中判定时调用)。
73
+ * @author ddj 2026年09月22号
74
+ * @param path 文件路径
75
+ * @returns true=已走官方原生打开;false=handler 缺失或失败(调用方回退现状)
76
+ */
77
+ export function tryNativeOpen(path: string): boolean {
78
+ if (!handler) return false
79
+ try {
80
+ return handler(path) === true
81
+ } catch {
82
+ return false
83
+ }
84
+ }
85
+
86
+ /** 复位(卸载/测试隔离用:回默认集、解绑 handler)。 */
87
+ export function resetNativeOpen(): void {
88
+ csv = DEFAULT_NATIVE_CSV
89
+ exts = new Set(DEFAULT_NATIVE_EXT)
90
+ handler = null
91
+ }
@@ -11,6 +11,12 @@
11
11
 
12
12
  import { log } from './log.js'
13
13
  import { setEnsureSideEditor } from './sidebarBridge.js'
14
+ import { OFFICE_EXT, BLIND_EXT, suffixOf } from '../shared/nativeOpen.js'
15
+ import { inNativePath } from './nativeOpenStore.js'
16
+
17
+ // 让位清单与 CSV/TSV 决策(可编辑性优先,同 avif 例外)已抽至双面契约 shared/nativeOpen.ts
18
+ //(host 的 Config schema 默认原生打开范围与本模块共用同一事实源);此处 re-export 保持旧 import 路径。
19
+ export { OFFICE_EXT, BLIND_EXT }
14
20
 
15
21
  /** 官方 Tab 类型注册表服务名(ctx.get 用;不进 inject——旧版 DSH 无此包,inject 会停等)。 */
16
22
  export const OFFICIAL_TABS_SERVICE = 'sidebarRightTabs'
@@ -33,55 +39,14 @@ export const OFFICIAL_FILE_TAB_ID = 'dsh-vscode-mode/file'
33
39
  /** file 资源类型的 kind(与页类型 edrvEditor 区分:同 kind 只允许一份 extension)。 */
34
40
  export const OFFICIAL_FILE_KIND = 'edrvEditorFile'
35
41
 
36
- /**
37
- * Office 文档后缀:官方 `dsh-client-ui-sidebar-documentpreview` 的 Office 渲染器
38
- * 声明 `doc/docx/xls/xlsx/ppt/pptx`,并在 alpha.2 起提供侧栏 Office 预览。
39
- * 本插件让位官方(不认领),否则会把这些文件路由进 Monaco 而丢掉 Office 预览。
40
- */
41
- export const OFFICE_EXT: readonly string[] = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp', 'pages', 'numbers']
42
-
43
- /**
44
- * 官方「不可预览二进制容器」清单(`UNVIEWABLE_BINARY_EXTENSIONS`,逐项取自
45
- * `dsh-client-ui-sidebar-documentpreview/lib/client.js` 的 `UNVIEWABLE_BINARY_EXTENSIONS`)。
46
- * 这些后缀官方会给出「无法预览」提示;本插件让位官方,避免把二进制当文本读成乱码。
47
- *
48
- * ⚠️ 刻意例外:官方清单含 `avif`,但本插件 {@link IMAGE_MIME} 支持 avif 图片预览
49
- * (浏览器原生解码),故从本表**移除** `avif` —— 保留本插件的图片预览优于官方的「不可预览」。
50
- * @author ddj 2026年09月18号
51
- */
52
- export const BLIND_EXT: readonly string[] = [
53
- // 媒体
54
- 'mp4', 'mov', 'avi', 'mkv', 'webm', 'flv', 'wmv', 'm4v', 'mp3', 'wav', 'flac', 'ogg', 'm4a', 'aac', 'wma', 'opus',
55
- // 归档
56
- 'zip', 'gz', 'tgz', 'bz2', 'xz', 'zst', '7z', 'rar', 'tar', 'jar',
57
- // 可执行与库
58
- 'exe', 'dll', 'so', 'dylib', 'bin', 'o', 'class', 'pyc', 'wasm',
59
- // 字体
60
- 'ttf', 'otf', 'woff', 'woff2', 'eot',
61
- // 磁盘映像
62
- 'dmg', 'iso', 'img',
63
- // 数据库与设计文件
64
- 'sqlite', 'db', 'psd', 'ai', 'sketch', 'tiff', 'tif', 'heic', 'heif',
65
- ]
66
-
67
42
  /** 让位判定用后缀集合(模块级构建一次;小写、无前导点)。 */
68
43
  const DEFER_EXT_SET: Set<string> = new Set([...OFFICE_EXT, ...BLIND_EXT])
69
44
 
70
- /**
71
- * 取 path 的 basename 后缀(小写、无点);无后缀/隐藏文件返回 ''。
72
- * @author ddj 2026年09月18号
73
- * @param path 文件路径(`/` 或 `\` 分隔均可)
74
- * @returns 小写后缀或 ''
75
- */
76
- function suffixOf(path: unknown): string {
77
- const base = String(path ?? '').replace(/\\/g, '/').split('/').pop() ?? ''
78
- const dot = base.lastIndexOf('.')
79
- return dot > 0 ? base.slice(dot + 1).toLowerCase() : ''
80
- }
81
-
82
45
  /**
83
46
  * 该路径是否应让位官方查看器(Office 文档 + 官方不可预览的二进制容器)。
84
47
  * 命中即不认领 `dsh-resource://file/**`,回落官方预览/提示。
48
+ * 注:用户经设置 nativeOpenExts 追加的范围不走本函数,由 claim 的 canOpen 与
49
+ * 文件树 openFile 叠加 nativeOpenStore 判定(见 registerOfficialFileClaim)。
85
50
  * @author ddj 2026年09月18号
86
51
  * @param path 文件路径(工作区相对或绝对)
87
52
  * @returns 是否让位官方
@@ -374,11 +339,12 @@ export function registerOfficialFileClaim(options: {
374
339
  kind: OFFICIAL_FILE_KIND,
375
340
  patterns: [OFFICIAL_FILE_PATTERN],
376
341
  priority: 'extension',
377
- // 畸形/解码失败地址否决 + Office/不可预览后缀让位官方:回落官方查看器,不产生白屏 Tab
342
+ // 畸形/解码失败地址否决 + Office/不可预览后缀让位官方 + 用户配置的原生打开范围
343
+ //(nativeOpenExts)同步让位——否则认领会把 openResource 又转发回本插件(原生打开失效)
378
344
  canOpen: (address: string) => {
379
345
  const parsed = parseOfficialFileAddress(address)
380
346
  if (parsed === null) return false
381
- return !deferToOfficial(parsed.path)
347
+ return !deferToOfficial(parsed.path) && !inNativePath(parsed.path)
382
348
  },
383
349
  title: (address: string) => officialFileTitle(address),
384
350
  })
@@ -0,0 +1,66 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — 「打开方式…」选择弹窗(资源管理器右键、文件目标专属)。
4
+ * 列出 fileOpeners 注册表中当前可用的打开器(本插件/官方侧栏/系统等),
5
+ * 选中即调用该打开器 open();复用 ModalShell 视觉,Esc/遮罩/取消 = 不打开。
6
+ * 作者 ddj 2026年09月22号
7
+ */
8
+ import React from 'react'
9
+ import { createRoot } from 'react-dom/client'
10
+ import { availableOpeners, isAvailable } from './fileOpeners.js'
11
+ import { ModalShell } from './ui/ModalShell.js'
12
+
13
+ /** 打开器列表行样式(列表为一次性选择器,不进 editor.css 的常驻样式表)。 */
14
+ const ITEM_STYLE = {
15
+ display: 'flex', flexDirection: 'column', gap: 2, width: '100%', textAlign: 'left',
16
+ padding: '8px 10px', marginBottom: 6, borderRadius: 6, cursor: 'pointer',
17
+ }
18
+
19
+ /** 打开器选择卡片(ModalShell 内容)。 */
20
+ function OpenWithCard({ openers, onPick, onCancel }) {
21
+ return React.createElement(React.Fragment, null,
22
+ React.createElement('h3', null, '打开方式…'),
23
+ openers.map((opener) => React.createElement('button', {
24
+ key: opener.id,
25
+ style: ITEM_STYLE,
26
+ title: opener.description || opener.label,
27
+ onClick: () => onPick(opener),
28
+ },
29
+ React.createElement('span', null, opener.label),
30
+ opener.description ? React.createElement('span', { style: { fontSize: 12, opacity: 0.7 } }, opener.description) : null)),
31
+ React.createElement('div', { className: 'vsm-mcp-dialog-actions' },
32
+ React.createElement('button', { onClick: onCancel }, '取消')))
33
+ }
34
+
35
+ /**
36
+ * 弹出打开器选择弹窗;选中后立即以该打开器打开目标文件。
37
+ * @author ddj 2026年09月22号
38
+ * @param registry 文件打开器注册表
39
+ * @param context 打开上下文(会话/工作区)
40
+ * @param path 目标文件(工作区相对)
41
+ * @param onDone 结果回调(opened = 实际使用的打开器;null = 取消/未打开),反馈文案由调用方接线
42
+ */
43
+ export function openWithDialog(registry, context, path, onDone) {
44
+ const openers = availableOpeners(registry)
45
+ if (!openers.length) {
46
+ onDone?.(null)
47
+ return
48
+ }
49
+ const host = document.createElement('div')
50
+ document.body.appendChild(host)
51
+ const root = createRoot(host)
52
+ const close = () => {
53
+ root.unmount()
54
+ host.remove()
55
+ }
56
+ const pick = (opener) => {
57
+ close()
58
+ if (!isAvailable(opener)) {
59
+ onDone?.(null)
60
+ return
61
+ }
62
+ Promise.resolve(opener.open(path, context)).then(() => onDone?.(opener)).catch(() => onDone?.(null))
63
+ }
64
+ root.render(React.createElement(ModalShell, { onClose: () => { close(); onDone?.(null) } },
65
+ React.createElement(OpenWithCard, { openers, onPick: pick, onCancel: () => { close(); onDone?.(null) } })))
66
+ }
@@ -5,7 +5,7 @@
5
5
  * 作者 ddj 2026-08-27
6
6
  */
7
7
  import React from 'react'
8
- import { IconCodeOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
8
+ import { IconCodeOutline16 } from '../ui/icons.js'
9
9
  import { OutlinePanel } from './OutlinePanel.js'
10
10
  import type { SidebarPanelDef, SidebarCtx } from '../sidebar/types.js'
11
11
 
@@ -28,6 +28,7 @@ export function createPdfPanel(deps) {
28
28
 
29
29
  let stage = null
30
30
  let pageLabel = null
31
+ let zoomLabel = null
31
32
  let eventBus = null
32
33
  let busAbort = null
33
34
  let editorTypes = null
@@ -52,6 +53,15 @@ export function createPdfPanel(deps) {
52
53
  pageLabel.textContent = pdfViewer.currentPageNumber + ' / ' + pdfViewer.pagesCount
53
54
  }
54
55
 
56
+ /**
57
+ * 缩放百分比指示更新(scalechanging/pagesinit 事件;与图片预览的 x% 文本同款对齐)。
58
+ * @author ddj 2026年09月22号
59
+ */
60
+ const updateZoomLabel = () => {
61
+ if (!zoomLabel || !(pdfViewer && pdfViewer.currentScale > 0)) return
62
+ zoomLabel.textContent = Math.round(pdfViewer.currentScale * 100) + '%'
63
+ }
64
+
55
65
  /** 编辑模式按钮高亮同步(annotationeditormodechanged 事件;evt.mode 为枚举数值)。 */
56
66
  const syncModeButtons = (mode) => {
57
67
  if (!modeButtons || !editorTypes) return
@@ -136,8 +146,13 @@ export function createPdfPanel(deps) {
136
146
  pageLabel.textContent = '…'
137
147
  bar.appendChild(pageLabel)
138
148
  mkBtn('▶', '下一页', () => { if (pdfViewer) pdfViewer.currentPageNumber = Math.min(pdfViewer.pagesCount, pdfViewer.currentPageNumber + 1) })
149
+ // 缩放语义与图片预览对齐(0.1.7 官方统一缩放):初始 page-width、edrv-pill 步进按钮与百分比指示同款
139
150
  mkBtn('−', '缩小', () => { try { pdfViewer.decreaseScale() } catch { /* 缩放失败忽略 */ } }, 'edrv-pill edrv-pill-ghost edrv-pdf-zoom')
140
151
  mkBtn('+', '放大', () => { try { pdfViewer.increaseScale() } catch { /* 缩放失败忽略 */ } }, 'edrv-pill edrv-pill-ghost edrv-pdf-zoom')
152
+ zoomLabel = document.createElement('span')
153
+ zoomLabel.className = 'edrv-pdf-page'
154
+ zoomLabel.textContent = '…'
155
+ bar.appendChild(zoomLabel)
141
156
  bar.appendChild(document.createElement('span')).className = 'edrv-pdf-sep'
142
157
  const modes = {}
143
158
  modes.freetext = mkBtn('✎ 文本框', '插入文本框注释', () => applyMode('freetext'))
@@ -190,9 +205,11 @@ export function createPdfPanel(deps) {
190
205
  })
191
206
  linkService.setViewer(pdfViewer)
192
207
  onBus('pagechanging', updatePageLabel)
208
+ onBus('scalechanging', updateZoomLabel)
193
209
  onBus('pagesinit', () => {
194
210
  try { pdfViewer.currentScaleValue = 'page-width' } catch { /* 初始缩放失败用默认值 */ }
195
211
  updatePageLabel()
212
+ updateZoomLabel()
196
213
  })
197
214
  onBus('annotationeditormodechanged', (evt) => syncModeButtons(evt?.mode))
198
215
 
@@ -233,6 +250,7 @@ export function createPdfPanel(deps) {
233
250
  pdfDocument = null
234
251
  loadingTask = null
235
252
  modeButtons = null
253
+ zoomLabel = null
236
254
  }
237
255
 
238
256
  return { mount, destroy, savePdf, isDirty }
@@ -62,3 +62,39 @@ export function takeSearchSeed(): string {
62
62
  export function clearSearchSeed(): void {
63
63
  pendingSeed = null
64
64
  }
65
+
66
+ // --region 目录过滤种子(「在文件夹中查找…」:SearchPanel 消费后置 include 过滤)
67
+
68
+ /** 待消费的目录过滤种子('' = 无)。 */
69
+ let pendingScope: string | null = null
70
+
71
+ /**
72
+ * 写入待消费的目录过滤种子(空目录等价于清除)。
73
+ * @author ddj 2026年09月22号
74
+ * @param dir 目标目录(工作区相对;空 = 根 = 不限定目录)
75
+ */
76
+ export function setSearchScope(dir: unknown): void {
77
+ const text = String(dir ?? '').trim().replace(/\\/g, '/').replace(/\/+$/, '')
78
+ pendingScope = text ? text : null
79
+ }
80
+
81
+ /**
82
+ * 取走待消费的目录过滤种子(取后即清空,与 query 种子同一一次性消费语义)。
83
+ * @author ddj 2026年09月22号
84
+ * @returns 目录相对路径;无待消费种子返回空串
85
+ */
86
+ export function takeSearchScope(): string {
87
+ const scope = pendingScope
88
+ pendingScope = null
89
+ return scope ?? ''
90
+ }
91
+
92
+ /**
93
+ * 清空待消费的目录过滤种子(测试隔离用)。
94
+ * @author ddj 2026年09月22号
95
+ */
96
+ export function clearSearchScope(): void {
97
+ pendingScope = null
98
+ }
99
+
100
+ // --endregion
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * dsh-vscode-mode client — 设置 scope 的跨组件上下文。
3
- * 绑定逻辑(webUiSettings 兼容桥 → 官方 settingsScope 降级)统一由 compat 层提供。
4
- * 作者 ddj 2026年08月24号
3
+ * 绑定逻辑(configForms 0.1.7+ → webUiSettings 兼容桥 → 官方 settingsScope 降级,
4
+ * 带晚到有界重试)统一由 compat 层提供。
5
+ * 作者 ddj 2026年08月24号 / 2026年09月22号
5
6
  */
6
7
  import React from 'react'
7
8
  import { pickSettingsBinder, type SettingsScopeLike } from './compat.js'
@@ -11,8 +12,9 @@ export type { SettingsScopeLike }
11
12
  export const SettingsContext = React.createContext<SettingsScopeLike | undefined>(undefined)
12
13
 
13
14
  /**
14
- * 从 Cordis 服务绑定 VSCodeMode 设置 scope(兼容桥优先,官方次之)。
15
- * @author ddj 2026年08月24号
15
+ * 从 Cordis 服务绑定 VSCodeMode 设置 scope(四级桥:configForms/webUiSettings/settingsScope/无)。
16
+ * 每次调用重新探测——设置提供方晚到时后续渲染自然拿到新桥(自愈)。
17
+ * @author ddj 2026年08月24号(2026年09月22号 增补 configForms 级)
16
18
  * @param ctx 客户端服务上下文
17
19
  * @returns 设置 scope 或 undefined
18
20
  */