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
package/src/rpc.ts
CHANGED
|
@@ -34,7 +34,7 @@ import { rulesList, rulesRead, rulesRemove, rulesSave, rulesToggle } from './rul
|
|
|
34
34
|
import { isSnippetFilePath, snippetsEntries, snippetsList, snippetsRead, snippetsRemove, snippetsSave } from './snippets.js'
|
|
35
35
|
import { listMcp, refreshMcp, removeMcp, saveMcp, toggleMcp } from './mcp.js'
|
|
36
36
|
import { listProjects, projectRefresh, projectRemove, projectSave, projectToggle } from './mcpProject.js'
|
|
37
|
-
import { normalizeFileOpenTool, FILE_OPEN_DEFAULT, FILE_OPEN_SETTINGS_NS } from './fileOpenSettings.js'
|
|
37
|
+
import { normalizeFileOpenTool, FILE_OPEN_DEFAULT, FILE_OPEN_SETTINGS_NS, sectionOf, updateSection } from './fileOpenSettings.js'
|
|
38
38
|
import { INTEGRATION_BASE_DEFAULT } from './shared/integration.js'
|
|
39
39
|
import { shellMenuRegister, shellMenuRemove, shellMenuStatus } from './integrate.js'
|
|
40
40
|
import { unityAdd, unityInstall, unityList, unityRemove } from './unityBridge.js'
|
|
@@ -44,10 +44,11 @@ import { findProfileDir, readDevForm, setDevForm } from './devForm.js'
|
|
|
44
44
|
import { normalizeRel } from './tree.js'
|
|
45
45
|
import { baseNameOf, checkNewName, checkRenameName, isSubPath, joinRelPath, parentRelOf } from './shared/fsNames.js'
|
|
46
46
|
import { invalidateIndex, listDirCached } from './treeIndex.js'
|
|
47
|
+
import { findRemoteWs, remoteDisplayOf, type RemoteWs } from './remoteWorkspace.js'
|
|
47
48
|
import { revealInExplorer } from './reveal.js'
|
|
48
49
|
import { dshHome, debugLogFile, pluginLogRoot } from './paths.js'
|
|
49
50
|
import { clearDebugLog, debugRecord, isDebugLogName, listDebugLogs, readDebugLog } from './debugLog.js'
|
|
50
|
-
import { markActiveSessions, moveOutSessions, planMoveOut, purgeArchive, restoreSession, scanSessionInventory, sessionsArchiveRoot, sessionSizeOf, sidecarSummaryOf } from './perf.js'
|
|
51
|
+
import { markActiveSessions, markOfficialFlags, moveOutSessions, planMoveOut, purgeArchive, restoreSession, scanSessionInventory, sessionsArchiveRoot, sessionSizeOf, sidecarSummaryOf, unarchiveOfficial } from './perf.js'
|
|
51
52
|
import { patchHasPerfConfig, patchInsertPerfConfig, patchRemovePerfConfig, perfConfigBlock } from './perfPatch.js'
|
|
52
53
|
import type { FileVersions } from './fileVersions.js'
|
|
53
54
|
|
|
@@ -56,8 +57,8 @@ const staleCheckedAt = new Map<string, number>()
|
|
|
56
57
|
/** stale 自动清理最小间隔。 */
|
|
57
58
|
const STALE_CHECK_MIN_MS = 10_000
|
|
58
59
|
|
|
59
|
-
/** 记录 → 客户端视图(不含 before
|
|
60
|
-
function recView(record: DiffRecord): RecordView {
|
|
60
|
+
/** 记录 → 客户端视图(不含 before 全文,仅长度);远程工作区附远端展示路径(issue #7)。 */
|
|
61
|
+
function recView(record: DiffRecord, ws: RemoteWs | null = null): RecordView {
|
|
61
62
|
return {
|
|
62
63
|
callId: record.callId,
|
|
63
64
|
toolName: record.toolName,
|
|
@@ -75,6 +76,7 @@ function recView(record: DiffRecord): RecordView {
|
|
|
75
76
|
conflict: record.conflict === true,
|
|
76
77
|
legacy: record.legacy === true,
|
|
77
78
|
at: record.at,
|
|
79
|
+
...(ws ? { displayPath: remoteDisplayOf(record.path, ws) ?? undefined } : {}),
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
|
|
@@ -107,6 +109,57 @@ function snippetTargetOf(path: string): string | null {
|
|
|
107
109
|
return isSnippetFilePath(path) ? path.replace(/\\/g, '/') : null
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
/** readTargetOf 成功形态:解析后的目标路径、stat 信息与 fs 服务句柄。 */
|
|
113
|
+
interface ReadTargetOk {
|
|
114
|
+
target: string
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
+
fs: any
|
|
117
|
+
info: { version?: unknown; size?: number }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 二进制/文本读取共用前置(edrv.read 与 edrv.readBinary):解析路径 + debugRecord +
|
|
122
|
+
* stat + 文件类型校验 + 32MB 二进制上限,保证两条读通道的错误口径完全一致。
|
|
123
|
+
* @author ddj 2026年09月22号
|
|
124
|
+
* @param ctx DSH 上下文
|
|
125
|
+
* @param sc requireSession 成功结果(session + cwd)
|
|
126
|
+
* @param path 客户端请求路径
|
|
127
|
+
* @returns 成功返回解析目标与 stat;失败返回错误文案(文件不存在时附 resolvedPath)
|
|
128
|
+
*/
|
|
129
|
+
async function readTargetOf(
|
|
130
|
+
ctx: Ctx,
|
|
131
|
+
sc: { session: Session; cwd: string },
|
|
132
|
+
path: string,
|
|
133
|
+
): Promise<ReadTargetOk | { err: string; resolvedPath?: string }> {
|
|
134
|
+
const fs = ctx.get('fs')
|
|
135
|
+
if (!fs) return { err: '缺少 fs' }
|
|
136
|
+
try {
|
|
137
|
+
const target = await resolveTarget(ctx, sc.session, path)
|
|
138
|
+
debugRecord(ctx, sc.cwd, '[DEBUG path.resolve] input=' + String(path ?? '') + ' resolved=' + fs.processPath(target), 'debug')
|
|
139
|
+
const info = await fs.stat(target)
|
|
140
|
+
if (!info || info.type !== 'file') {
|
|
141
|
+
// 带上解析后的真实路径:跳转失败时可直接看出是路径解析错还是目标本身不存在
|
|
142
|
+
return { err: '文件不存在', resolvedPath: fs.processPath(target) }
|
|
143
|
+
}
|
|
144
|
+
if ((info.size ?? 0) > BINARY_READ_CAP) return { err: '文件过大(>32MB),不支持整文件预览' }
|
|
145
|
+
return { target, fs, info }
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return { err: '读取失败:' + String(error) }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* readTargetOf 失败结果 → RPC 错误载荷(文件不存在时保留 resolvedPath 供诊断)。
|
|
153
|
+
* @author ddj 2026年09月22号
|
|
154
|
+
* @param prep readTargetOf 的失败分支
|
|
155
|
+
* @returns { ok:false } 形态载荷
|
|
156
|
+
*/
|
|
157
|
+
function targetErrOf(prep: { err: string; resolvedPath?: string }): { ok: false; error: string; resolvedPath?: string } {
|
|
158
|
+
return prep.resolvedPath !== undefined
|
|
159
|
+
? { ok: false, error: prep.err, resolvedPath: prep.resolvedPath }
|
|
160
|
+
: { ok: false, error: prep.err }
|
|
161
|
+
}
|
|
162
|
+
|
|
110
163
|
/**
|
|
111
164
|
* 本地文件系统版本令牌(mtime+size):与 ctx.fs 的不透明版本串用途相同,
|
|
112
165
|
* 仅供全局片段文件(走 node:fs 直读、不经 ctx.fs)在 edrv.read 时回带。
|
|
@@ -181,6 +234,7 @@ async function applyDecisions(
|
|
|
181
234
|
): Promise<DecideResult[]> {
|
|
182
235
|
const results: DecideResult[] = []
|
|
183
236
|
const resolved: DiffRecord[] = []
|
|
237
|
+
const ws = findRemoteWs(cwd)
|
|
184
238
|
let changed = false
|
|
185
239
|
for (const item of items) {
|
|
186
240
|
const record = bucket.get(item.callId)
|
|
@@ -204,7 +258,7 @@ async function applyDecisions(
|
|
|
204
258
|
markDecision(record, item.scope ?? 'call', item.hunkIndex, item.decision)
|
|
205
259
|
changed = true
|
|
206
260
|
if (!wasResolved && recordResolved(record)) resolved.push(record)
|
|
207
|
-
const result: DecideResult = { callId: item.callId, ok: true, record: recView(record) }
|
|
261
|
+
const result: DecideResult = { callId: item.callId, ok: true, record: recView(record, ws) }
|
|
208
262
|
if (revertedStale) result.stale = true
|
|
209
263
|
results.push(result)
|
|
210
264
|
}
|
|
@@ -241,8 +295,7 @@ async function latestPatchBackup(patchPath: string): Promise<string | undefined>
|
|
|
241
295
|
/** 读取设置中的深链基址(缺省/非法回退默认 3080)。 */
|
|
242
296
|
async function integrationBaseUrlOf(ctx: Ctx): Promise<string> {
|
|
243
297
|
const settings = ctx.get('settings')
|
|
244
|
-
const
|
|
245
|
-
const value = descriptor?.value as { integrationBaseUrl?: unknown } | undefined
|
|
298
|
+
const value = sectionOf(settings, FILE_OPEN_SETTINGS_NS)?.value as { integrationBaseUrl?: unknown } | undefined
|
|
246
299
|
const raw = typeof value?.integrationBaseUrl === 'string' ? value.integrationBaseUrl.trim() : ''
|
|
247
300
|
return raw || INTEGRATION_BASE_DEFAULT
|
|
248
301
|
}
|
|
@@ -384,11 +437,12 @@ export function buildHandlers(
|
|
|
384
437
|
}
|
|
385
438
|
}
|
|
386
439
|
const out: RecordView[] = []
|
|
440
|
+
const ws = findRemoteWs(sc.cwd)
|
|
387
441
|
for (const rec of bucket.values()) {
|
|
388
442
|
// 面板全量查询过滤已归档;聊天条按 callId 查询保留(状态徽章仍需正确显示)
|
|
389
443
|
if (!want && rec.archived) continue
|
|
390
444
|
if (want && !want.has(rec.callId)) continue
|
|
391
|
-
out.push(recView(rec))
|
|
445
|
+
out.push(recView(rec, ws))
|
|
392
446
|
}
|
|
393
447
|
out.sort((a, b) => (a.at < b.at ? -1 : 1))
|
|
394
448
|
return { ok: true, records: out }
|
|
@@ -433,26 +487,36 @@ export function buildHandlers(
|
|
|
433
487
|
}
|
|
434
488
|
const sc = await requireSession(ctx, args.sessionId)
|
|
435
489
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
436
|
-
const
|
|
437
|
-
if (
|
|
490
|
+
const prep = await readTargetOf(ctx, sc, args.path)
|
|
491
|
+
if ('err' in prep) return targetErrOf(prep)
|
|
438
492
|
try {
|
|
439
|
-
const target = await resolveTarget(ctx, sc.session, args.path)
|
|
440
|
-
debugRecord(ctx, sc.cwd, '[DEBUG path.resolve] input=' + String(args.path ?? '') + ' resolved=' + fs.processPath(target), 'debug')
|
|
441
|
-
const info = await fs.stat(target)
|
|
442
|
-
if (!info || info.type !== 'file') {
|
|
443
|
-
// 带上解析后的真实路径:跳转失败时可直接看出是路径解析错还是目标本身不存在
|
|
444
|
-
return { ok: false, error: '文件不存在', resolvedPath: fs.processPath(target) }
|
|
445
|
-
}
|
|
446
|
-
if ((info.size ?? 0) > BINARY_READ_CAP) return { ok: false, error: '文件过大(>32MB),不支持整文件预览' }
|
|
447
493
|
if (args.encoding === 'base64') {
|
|
448
|
-
// 图片/PDF 等二进制预览:readBytes
|
|
449
|
-
const bytes = await fs.readBytes(target, undefined, BINARY_READ_CAP)
|
|
494
|
+
// 图片/PDF 等二进制预览:readBytes 无解码、无二进制拒绝;超上限已由 readTargetOf 拦截
|
|
495
|
+
const bytes = await prep.fs.readBytes(prep.target, undefined, BINARY_READ_CAP)
|
|
450
496
|
const content = Buffer.from(bytes).toString('base64')
|
|
451
|
-
return { ok: true, content, size: bytes.byteLength, encoding: 'base64', mime: binaryMimeOf(args.path), version: String(info.version ?? '') }
|
|
497
|
+
return { ok: true, content, size: bytes.byteLength, encoding: 'base64', mime: binaryMimeOf(args.path), version: String(prep.info.version ?? '') }
|
|
498
|
+
}
|
|
499
|
+
if ((prep.info.size ?? 0) > READ_CAP) return { ok: false, error: '文件过大(>8MB),不支持整文件预览' }
|
|
500
|
+
const content = await prep.fs.readText(prep.target)
|
|
501
|
+
return { ok: true, content, size: content.length, version: String(prep.info.version ?? '') }
|
|
502
|
+
} catch (error) {
|
|
503
|
+
return { ok: false, error: '读取失败:' + String(error) }
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
'edrv.readBinary': async (args) => {
|
|
507
|
+
// 二进制直读通道(图片/PDF 预览):与 edrv.read base64 分支共用 readTargetOf 前置,
|
|
508
|
+
// 返回 Uint8Array 信封;routes 接线后以 octet-stream + x-edrv-* 头直出,
|
|
509
|
+
// 未接线时信封被 JSON 化,由 client 探测到后自动回退 base64 路径(见 shared/rpc readBinaryPreview)。
|
|
510
|
+
const sc = await requireSession(ctx, args.sessionId)
|
|
511
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
512
|
+
const prep = await readTargetOf(ctx, sc, args.path)
|
|
513
|
+
if ('err' in prep) return targetErrOf(prep)
|
|
514
|
+
try {
|
|
515
|
+
const bytes = await prep.fs.readBytes(prep.target, undefined, BINARY_READ_CAP)
|
|
516
|
+
return {
|
|
517
|
+
ok: true,
|
|
518
|
+
binary: { bytes, mime: binaryMimeOf(args.path), size: bytes.byteLength, version: String(prep.info.version ?? '') },
|
|
452
519
|
}
|
|
453
|
-
if ((info.size ?? 0) > READ_CAP) return { ok: false, error: '文件过大(>8MB),不支持整文件预览' }
|
|
454
|
-
const content = await fs.readText(target)
|
|
455
|
-
return { ok: true, content, size: content.length, version: String(info.version ?? '') }
|
|
456
520
|
} catch (error) {
|
|
457
521
|
return { ok: false, error: '读取失败:' + String(error) }
|
|
458
522
|
}
|
|
@@ -550,6 +614,7 @@ export function buildHandlers(
|
|
|
550
614
|
const sc = await requireSession(ctx, args.sessionId)
|
|
551
615
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
552
616
|
const batches = parseArchive(await readArchiveText(ctx, sc.cwd)).filter((b) => b.cwd === sc.cwd)
|
|
617
|
+
const ws = findRemoteWs(sc.cwd)
|
|
553
618
|
const entries = batches.map((b) => {
|
|
554
619
|
const recs = Array.isArray(b.records) ? b.records : []
|
|
555
620
|
const sum = recs.reduce((s, r) => {
|
|
@@ -560,7 +625,16 @@ export function buildHandlers(
|
|
|
560
625
|
if (sm.superseded) s.superseded++
|
|
561
626
|
return s
|
|
562
627
|
}, { accepted: 0, rejected: 0, pending: 0, superseded: 0 })
|
|
563
|
-
return {
|
|
628
|
+
return {
|
|
629
|
+
at: b.at,
|
|
630
|
+
lastAt: b.lastAt || b.at,
|
|
631
|
+
path: b.path,
|
|
632
|
+
...(ws ? { displayPath: remoteDisplayOf(b.path, ws) ?? undefined } : {}),
|
|
633
|
+
batch: b.batch ?? null,
|
|
634
|
+
reason: b.reason ?? null,
|
|
635
|
+
nRecords: recs.length,
|
|
636
|
+
summary: sum,
|
|
637
|
+
}
|
|
564
638
|
})
|
|
565
639
|
entries.sort((a, b) => (Number(b.batch ?? -1) - Number(a.batch ?? -1)) || (a.at < b.at ? 1 : -1))
|
|
566
640
|
return { ok: true, entries }
|
|
@@ -847,22 +921,20 @@ export function buildHandlers(
|
|
|
847
921
|
},
|
|
848
922
|
'vscode.fileOpenSettingsGet': async () => {
|
|
849
923
|
const settings = ctx.get('settings')
|
|
850
|
-
const
|
|
851
|
-
const value =
|
|
852
|
-
return { ok: true, fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool ?? FILE_OPEN_DEFAULT), integrationBaseUrl: await integrationBaseUrlOf(ctx), revision:
|
|
924
|
+
const section = sectionOf(settings, FILE_OPEN_SETTINGS_NS)
|
|
925
|
+
const value = section?.value as { fileOpenTool?: unknown } | undefined
|
|
926
|
+
return { ok: true, fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool ?? FILE_OPEN_DEFAULT), integrationBaseUrl: await integrationBaseUrlOf(ctx), revision: section?.revision }
|
|
853
927
|
},
|
|
854
928
|
'vscode.fileOpenSettingsUpdate': async (args) => {
|
|
855
929
|
const settings = ctx.get('settings')
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
} catch (error) { return { ok: false, error: String(error) }
|
|
865
|
-
}
|
|
930
|
+
const patch: Record<string, unknown> = { fileOpenTool: normalizeFileOpenTool(args.fileOpenTool) }
|
|
931
|
+
if (typeof args.integrationBaseUrl === 'string' && args.integrationBaseUrl.trim()) patch.integrationBaseUrl = args.integrationBaseUrl.trim()
|
|
932
|
+
// 冲突自愈(0.1.7 SettingsConflictError 重读重试一次)与形状校验读取统一走 fileOpenSettings 工具
|
|
933
|
+
const result = await updateSection(settings, FILE_OPEN_SETTINGS_NS, patch, args.expectedRevision)
|
|
934
|
+
if (!result.ok) return { ok: false, error: result.error ?? '设置服务不可用' }
|
|
935
|
+
const section = sectionOf(settings, FILE_OPEN_SETTINGS_NS)
|
|
936
|
+
const value = section?.value as { fileOpenTool?: unknown } | undefined
|
|
937
|
+
return { ok: true, fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool), integrationBaseUrl: await integrationBaseUrlOf(ctx), revision: section?.revision }
|
|
866
938
|
},
|
|
867
939
|
'compat': async () => ({ ok: true, report: await buildReport(ctx) }),
|
|
868
940
|
'vscode.devFormGet': async () => ({ ok: true, devForm: readDevForm() }),
|
|
@@ -953,6 +1025,7 @@ export function buildHandlers(
|
|
|
953
1025
|
const inventory = await scanSessionInventory(home, sessionsArchiveRoot(home))
|
|
954
1026
|
const active = activeSessionIds(ctx)
|
|
955
1027
|
markActiveSessions(inventory.sessions, active)
|
|
1028
|
+
markOfficialFlags(inventory.sessions, ctx)
|
|
956
1029
|
return { ok: true, ...inventory, activeIds: [...active] }
|
|
957
1030
|
} catch (error) {
|
|
958
1031
|
return { ok: false, error: '盘点失败:' + String(error) }
|
|
@@ -990,7 +1063,10 @@ export function buildHandlers(
|
|
|
990
1063
|
'edrv.perf.restore': async (args) => {
|
|
991
1064
|
try {
|
|
992
1065
|
const result = await restoreSession(dshHome(), sessionsArchiveRoot(dshHome()), args.workspaceKey, args.sessionId)
|
|
993
|
-
|
|
1066
|
+
if (!result.ok) return { ok: false, error: result.error ?? '恢复失败' }
|
|
1067
|
+
// 目录搬回后对齐官方归档标志(best-effort:服务缺失/调用失败不影响恢复结果,见 perf.unarchiveOfficial)
|
|
1068
|
+
await unarchiveOfficial(ctx, args.sessionId)
|
|
1069
|
+
return { ok: true, restored: true }
|
|
994
1070
|
} catch (error) {
|
|
995
1071
|
return { ok: false, error: '恢复失败:' + String(error) }
|
|
996
1072
|
}
|
package/src/shared/ai.ts
CHANGED
|
@@ -66,6 +66,12 @@ export interface AiConfigView {
|
|
|
66
66
|
model: string
|
|
67
67
|
/** 空 = 跟随模型默认(请求不携带 reasoningEffort)。 */
|
|
68
68
|
effort: string
|
|
69
|
+
/** AI 任务模型(非补全场景):空/缺省 = 跟随补全配置。 */
|
|
70
|
+
taskProvider?: string
|
|
71
|
+
/** AI 任务模型 id(与 taskProvider 成对;空/缺省 = 跟随补全配置)。 */
|
|
72
|
+
taskModel?: string
|
|
73
|
+
/** AI 任务模型思考档位(空/缺省 = 跟随补全配置的 effort)。 */
|
|
74
|
+
taskEffort?: string
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
/** AI 补全配置更新载荷(edrv.ai.configUpdate)。 */
|
|
@@ -75,6 +81,12 @@ export interface AiConfigPatch {
|
|
|
75
81
|
model?: string
|
|
76
82
|
/** 空 = 清除(跟随默认)。 */
|
|
77
83
|
effort?: string
|
|
84
|
+
/** AI 任务模型(非补全场景):空 = 清除(跟随补全配置)。 */
|
|
85
|
+
taskProvider?: string
|
|
86
|
+
/** AI 任务模型 id(与 taskProvider 成对更新)。 */
|
|
87
|
+
taskModel?: string
|
|
88
|
+
/** AI 任务模型思考档位:空 = 清除(跟随补全配置的 effort)。 */
|
|
89
|
+
taskEffort?: string
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
/**
|
package/src/shared/lsp.ts
CHANGED
|
@@ -125,6 +125,82 @@ export function decodeSemanticTokens(
|
|
|
125
125
|
return out
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* LSP CompletionItemKind(协议 1-based)→ Monaco CompletionItemKind 枚举名(0-based)。
|
|
130
|
+
*
|
|
131
|
+
* 为什么按**名称**而不是编号直传:两侧枚举编号并不一致 —— LSP 2=Method,而 Monaco
|
|
132
|
+
* 0=Method。若把协议编号当 Monaco 编号塞回去,草稿纸上的「只差一位」会变成全表错位
|
|
133
|
+
* (属性显示成方法图标等)。用名称映射让两侧各自的编号在各自类型系统里解析,杜绝硬编码。
|
|
134
|
+
* 索引 i 对应 LSP kind i(索引 0 占位,协议从 1 起)。
|
|
135
|
+
* @author ddj 2026年09月22号
|
|
136
|
+
*/
|
|
137
|
+
export const LSP_COMPLETION_KIND_NAMES = [
|
|
138
|
+
'', 'Text', 'Method', 'Function', 'Constructor', 'Field', 'Variable', 'Class',
|
|
139
|
+
'Interface', 'Module', 'Property', 'Unit', 'Value', 'Enum', 'Keyword', 'Snippet',
|
|
140
|
+
'Color', 'File', 'Reference', 'Folder', 'EnumMember', 'Constant', 'Struct',
|
|
141
|
+
'Event', 'Operator', 'TypeParameter',
|
|
142
|
+
] as const
|
|
143
|
+
|
|
144
|
+
/** LSP InsertTextFormat:1=PlainText,2=Snippet。 */
|
|
145
|
+
export const LSP_INSERT_TEXT_FORMAT_SNIPPET = 2
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* LSP 补全项文本编辑范围(单 range 或 insert/replace 双 range 形态)。
|
|
149
|
+
* 两种形态由服务器任选,必须都能解析:只认一种会让另一家服务器的补全落点错位。
|
|
150
|
+
*/
|
|
151
|
+
export interface LspCompletionTextEdit {
|
|
152
|
+
range?: LspRange
|
|
153
|
+
insert?: LspRange
|
|
154
|
+
replace?: LspRange
|
|
155
|
+
newText: string
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 归一化后的补全项(host 归一化;client 映射为 Monaco suggestion)。 */
|
|
159
|
+
export interface LspCompletionItem {
|
|
160
|
+
label: string
|
|
161
|
+
kind?: number // LSP CompletionItemKind 原始编号(client 经名称表转 Monaco)
|
|
162
|
+
detail?: string
|
|
163
|
+
documentation?: string
|
|
164
|
+
insertText?: string
|
|
165
|
+
insertTextFormat?: number
|
|
166
|
+
textEdit?: LspCompletionTextEdit
|
|
167
|
+
additionalTextEdits?: Array<{ range: LspRange; newText: string }>
|
|
168
|
+
sortText?: string
|
|
169
|
+
filterText?: string
|
|
170
|
+
preselect?: boolean
|
|
171
|
+
commitCharacters?: string[]
|
|
172
|
+
deprecated?: boolean
|
|
173
|
+
data?: unknown // 原样透传,completionItem/resolve 需回传
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 归一化后的补全列表(incomplete:服务器要求按已输入内容重查)。 */
|
|
177
|
+
export interface LspCompletionList {
|
|
178
|
+
items: LspCompletionItem[]
|
|
179
|
+
incomplete: boolean
|
|
180
|
+
truncated?: boolean
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 签名帮助中的单个参数(label 可为字符串或 [start, end] 元组,原样透传)。 */
|
|
184
|
+
export interface LspParameterInformation {
|
|
185
|
+
label: string | [number, number]
|
|
186
|
+
documentation?: string
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** 归一化后的单个签名。 */
|
|
190
|
+
export interface LspSignatureInformation {
|
|
191
|
+
label: string
|
|
192
|
+
documentation?: string
|
|
193
|
+
parameters: LspParameterInformation[]
|
|
194
|
+
activeParameter?: number
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 归一化后的签名帮助结果。 */
|
|
198
|
+
export interface LspSignatureHelp {
|
|
199
|
+
signatures: LspSignatureInformation[]
|
|
200
|
+
activeSignature: number
|
|
201
|
+
activeParameter: number
|
|
202
|
+
}
|
|
203
|
+
|
|
128
204
|
/** LSP 服务器能力子集:本插件会用到的方法。 */
|
|
129
205
|
export interface LspServerCapabilities {
|
|
130
206
|
definition: boolean
|
|
@@ -134,6 +210,9 @@ export interface LspServerCapabilities {
|
|
|
134
210
|
workspaceSymbol: boolean
|
|
135
211
|
hover: boolean
|
|
136
212
|
semanticTokens: boolean
|
|
213
|
+
completion: boolean
|
|
214
|
+
completionResolve: boolean
|
|
215
|
+
signatureHelp: boolean
|
|
137
216
|
semanticTokenTypes?: string[]
|
|
138
217
|
semanticTokenModifiers?: string[]
|
|
139
218
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode shared — 原生打开范围判定(host/client 双面契约)。
|
|
3
|
+
* 背景:DSH 文件预览双形态——本插件认领 `dsh-resource://file/**` 进 Monaco 编辑,
|
|
4
|
+
* 官方渲染器(Office 预览/不可预览提示/表格预览)看认领让位。本模块承载「原生打开」
|
|
5
|
+
* 范围的单一事实源:默认集 = 让位清单(插件本来就不认领的后缀),用户可经设置
|
|
6
|
+
* `nativeOpenExts`(逗号分隔)增删;host 的 Config schema 默认值与 client 的
|
|
7
|
+
* 文件树点击判定 / claim 认领判定共用,保证三处永不同源漂移。
|
|
8
|
+
* 决策沿袭(不因本模块迁移而变):CSV/TSV **不在**默认集——官方 0.1.7 有表格预览,
|
|
9
|
+
* 但编辑器可编辑性优先,仅用户显式加入 nativeOpenExts 才让位(同 avif 例外先例)。
|
|
10
|
+
* 作者 ddj 2026年09月22号
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Office 文档后缀:官方 `dsh-client-ui-sidebar-documentpreview` 的 Office 渲染器
|
|
15
|
+
* 声明 `doc/docx/xls/xlsx/ppt/pptx`(0.1.6 起侧栏 Office 预览;0.1.7 扩展表格 CSV/TSV
|
|
16
|
+
* 只读预览——本清单刻意不含 csv/tsv,见文件头决策)。本插件让位官方(不认领),
|
|
17
|
+
* 否则会把这些文件路由进 Monaco 而丢掉官方预览。
|
|
18
|
+
*/
|
|
19
|
+
export const OFFICE_EXT: readonly string[] = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp', 'pages', 'numbers']
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 官方「不可预览二进制容器」清单(`UNVIEWABLE_BINARY_EXTENSIONS`,逐项取自
|
|
23
|
+
* `dsh-client-ui-sidebar-documentpreview/lib/client.js`)。这些后缀官方会给出
|
|
24
|
+
* 「无法预览」提示;本插件让位官方,避免把二进制当文本读成乱码。
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ 刻意例外:官方清单含 `avif`,但本插件图片预览支持 avif(浏览器原生解码),
|
|
27
|
+
* 故从本表**移除** `avif` —— 保留本插件的图片预览优于官方的「不可预览」。
|
|
28
|
+
* @author ddj 2026年09月18号
|
|
29
|
+
*/
|
|
30
|
+
export const BLIND_EXT: readonly string[] = [
|
|
31
|
+
// 媒体
|
|
32
|
+
'mp4', 'mov', 'avi', 'mkv', 'webm', 'flv', 'wmv', 'm4v', 'mp3', 'wav', 'flac', 'ogg', 'm4a', 'aac', 'wma', 'opus',
|
|
33
|
+
// 归档
|
|
34
|
+
'zip', 'gz', 'tgz', 'bz2', 'xz', 'zst', '7z', 'rar', 'tar', 'jar',
|
|
35
|
+
// 可执行与库
|
|
36
|
+
'exe', 'dll', 'so', 'dylib', 'bin', 'o', 'class', 'pyc', 'wasm',
|
|
37
|
+
// 字体
|
|
38
|
+
'ttf', 'otf', 'woff', 'woff2', 'eot',
|
|
39
|
+
// 磁盘映像
|
|
40
|
+
'dmg', 'iso', 'img',
|
|
41
|
+
// 数据库与设计文件
|
|
42
|
+
'sqlite', 'db', 'psd', 'ai', 'sketch', 'tiff', 'tif', 'heic', 'heif',
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
/** 原生打开默认后缀集 = 让位清单并集(两清单元素天然互斥,直接拼接)。 */
|
|
46
|
+
export const DEFAULT_NATIVE_EXT: readonly string[] = [...OFFICE_EXT, ...BLIND_EXT]
|
|
47
|
+
|
|
48
|
+
/** 默认范围的设置序列(逗号分隔;Config schema 默认值与设置页回显同源)。 */
|
|
49
|
+
export const DEFAULT_NATIVE_CSV: string = DEFAULT_NATIVE_EXT.join(',')
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 取 path 的 basename 后缀(小写、无点);无后缀/隐藏文件返回 ''。
|
|
53
|
+
* @author ddj 2026年09月22号
|
|
54
|
+
* @param path 文件路径(`/` 或 `\` 分隔均可)
|
|
55
|
+
* @returns 小写后缀或 ''
|
|
56
|
+
*/
|
|
57
|
+
export function suffixOf(path: unknown): string {
|
|
58
|
+
const base = String(path ?? '').replace(/\\/g, '/').split('/').pop() ?? ''
|
|
59
|
+
const dot = base.lastIndexOf('.')
|
|
60
|
+
return dot > 0 ? base.slice(dot + 1).toLowerCase() : ''
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 解析设置串为后缀集合:逗号分隔、去空白、去前导点、小写;空段丢弃。
|
|
65
|
+
* 解析失败/空串 → 空集合(= 不追加任何原生打开范围,让位仍走 deferToOfficial)。
|
|
66
|
+
* @author ddj 2026年09月22号
|
|
67
|
+
* @param csv 逗号分隔后缀串
|
|
68
|
+
* @returns 后缀集合
|
|
69
|
+
*/
|
|
70
|
+
export function parseExtCsv(csv: unknown): Set<string> {
|
|
71
|
+
const out = new Set<string>()
|
|
72
|
+
if (typeof csv !== 'string') return out
|
|
73
|
+
for (const raw of csv.split(',')) {
|
|
74
|
+
const item = raw.trim().replace(/^\./, '').toLowerCase()
|
|
75
|
+
if (item) out.add(item)
|
|
76
|
+
}
|
|
77
|
+
return out
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 该路径是否命中「原生打开」范围(仅看用户配置集合;默认集语义由调用方
|
|
82
|
+
* 与 deferToOfficial 取并,保持两判定各司其职)。
|
|
83
|
+
* @author ddj 2026年09月22号
|
|
84
|
+
* @param path 文件路径
|
|
85
|
+
* @param exts 已解析的后缀集合(parseExtCsv 产物)
|
|
86
|
+
* @returns 是否命中
|
|
87
|
+
*/
|
|
88
|
+
export function inNativeOpen(path: unknown, exts: ReadonlySet<string>): boolean {
|
|
89
|
+
const ext = suffixOf(path)
|
|
90
|
+
return ext !== '' && exts.has(ext)
|
|
91
|
+
}
|