dsh-vscode-mode 0.7.0 → 0.9.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 +37 -6
- package/assets/icon.svg +7 -0
- package/cordis.patch.yml +16 -0
- package/lib/client.js +2804 -300
- package/lib/client.js.map +1 -1
- package/lib/index.js +1975 -191
- package/lib/index.js.map +1 -1
- package/locale/en.json +4 -0
- package/locale/zh.json +4 -0
- package/package.json +4 -2
- package/src/ai/inline.ts +19 -2
- package/src/ai/svnTriage.ts +150 -0
- package/src/capture.ts +225 -51
- package/src/client/addToConversation.ts +203 -1
- package/src/client/compat.ts +134 -4
- package/src/client/imagePreview.ts +76 -2
- package/src/client/index.ts +65 -36
- package/src/client/monaco/lsp/lspClient.ts +46 -0
- package/src/client/monaco/lsp/providers.ts +245 -1
- package/src/client/nativeOpenStore.ts +91 -0
- package/src/client/officialSidebar.ts +11 -45
- package/src/client/outline/index.ts +1 -1
- package/src/client/pdf/pdfPanel.ts +18 -0
- package/src/client/pendingNav.ts +141 -0
- package/src/client/settingsContext.ts +6 -4
- package/src/client/sidebar/panels/FileExplorer.ts +2 -15
- package/src/client/sidebar/panels/SvnPanel.ts +682 -72
- package/src/client/sidebar/panels/index.ts +1 -1
- package/src/client/sidebar/panels/svnListBudget.ts +58 -0
- package/src/client/state/records.ts +7 -1
- package/src/client/styles/editor.css +47 -5
- package/src/client/svnStatus.ts +206 -1
- package/src/client/svnStore.ts +6 -1
- package/src/client/ui/AiSettings.ts +47 -14
- package/src/client/ui/DiffBox.ts +1 -1
- package/src/client/ui/DiffLauncher.ts +6 -4
- package/src/client/ui/EditorView.ts +149 -22
- package/src/client/ui/McpSettings.ts +30 -0
- package/src/client/ui/PerfSettings.ts +4 -1
- package/src/client/ui/SvnAiPlanDialog.ts +293 -0
- package/src/client/ui/icons.ts +91 -0
- package/src/compat.ts +26 -5
- package/src/dshVersion.ts +2 -1
- package/src/fileOpenSettings.ts +274 -49
- package/src/index.ts +18 -1
- package/src/lsp/client.ts +24 -0
- package/src/lsp/rpc.ts +57 -0
- package/src/lsp/server.ts +272 -2
- package/src/model.ts +2 -1
- package/src/perf.ts +90 -4
- package/src/remoteWorkspace.ts +177 -0
- package/src/routes.ts +13 -1
- package/src/rpc.ts +115 -39
- package/src/shared/ai.ts +12 -0
- package/src/shared/lsp.ts +79 -0
- package/src/shared/nativeOpen.ts +91 -0
- package/src/shared/rpc.ts +162 -2
- package/src/shared/svn.ts +296 -10
- package/src/shared/types.ts +5 -1
- package/src/svn.ts +235 -13
|
@@ -171,6 +171,52 @@ export async function fetchHover(path, text, position) {
|
|
|
171
171
|
return res.hover || null
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* 查询补全列表(列表阶段不带 documentation,由 resolve 补全)。
|
|
176
|
+
* @author ddj 2026年09月22号
|
|
177
|
+
* @param path 工作区相对路径
|
|
178
|
+
* @param text 文档全文(未保存内容必须进服务器,否则按旧文本解析)
|
|
179
|
+
* @param position Monaco 位置(1-based)
|
|
180
|
+
* @param context LSP CompletionContext(触发类型/触发字符)
|
|
181
|
+
* @returns { items, incomplete };失败或空时 null
|
|
182
|
+
*/
|
|
183
|
+
export async function fetchCompletions(path, text, position, context) {
|
|
184
|
+
await syncDoc(path, text, true)
|
|
185
|
+
const args = { path, position: monoToLsp(position.lineNumber, position.column) }
|
|
186
|
+
if (context) args.context = context
|
|
187
|
+
const res = await queryLsp('查询补全', 'edrv.lsp.completion', args)
|
|
188
|
+
if (!res || !res.completions) return null
|
|
189
|
+
return res.completions
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 惰性补全单个候选项(completionItem/resolve)。
|
|
194
|
+
* @author ddj 2026年09月22号
|
|
195
|
+
* @param path 工作区相对路径
|
|
196
|
+
* @param item host 归一化的补全项(含 data)
|
|
197
|
+
* @returns 补全后的条目;失败时回落原条目
|
|
198
|
+
*/
|
|
199
|
+
export async function resolveCompletion(path, item) {
|
|
200
|
+
const res = await queryLsp('解析补全', 'edrv.lsp.resolveCompletion', { path, item })
|
|
201
|
+
if (!res || !res.item) return item
|
|
202
|
+
return res.item
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 查询签名帮助。
|
|
207
|
+
* @author ddj 2026年09月22号
|
|
208
|
+
* @param path 工作区相对路径
|
|
209
|
+
* @param text 文档全文
|
|
210
|
+
* @param position Monaco 位置(1-based)
|
|
211
|
+
* @returns 归一化签名帮助;无签名时 null
|
|
212
|
+
*/
|
|
213
|
+
export async function fetchSignatureHelp(path, text, position) {
|
|
214
|
+
await syncDoc(path, text, true)
|
|
215
|
+
const res = await queryLsp('查询签名', 'edrv.lsp.signatureHelp', { path, position: monoToLsp(position.lineNumber, position.column) })
|
|
216
|
+
if (!res) return null
|
|
217
|
+
return res.signatureHelp || null
|
|
218
|
+
}
|
|
219
|
+
|
|
174
220
|
/** 查询当前文档的 semantic tokens(host 已归一化 legend 与 delta data)。 */
|
|
175
221
|
export async function fetchSemanticTokens(path, text) {
|
|
176
222
|
await syncDoc(path, text, true)
|
|
@@ -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 {
|
|
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
|
|
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
|
})
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 作者 ddj 2026-08-27
|
|
6
6
|
*/
|
|
7
7
|
import React from 'react'
|
|
8
|
-
import { IconCodeOutline16 } from '
|
|
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 }
|