dsh-vscode-mode 0.15.8 → 0.16.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 +6 -4
- package/lib/client.js +312 -69
- package/lib/client.js.map +1 -1
- package/lib/index.js +262 -17
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +4 -0
- package/src/client/state/editorFindState.ts +15 -10
- package/src/client/state/editorViewState.ts +7 -4
- package/src/client/styles/editor.css +3 -0
- package/src/client/ui/EditorPane.ts +23 -3
- package/src/client/ui/EditorView.ts +117 -15
- package/src/client/ui/McpSettings.ts +36 -0
- package/src/client/updateNotice.ts +64 -0
- package/src/dap/discovery.ts +5 -3
- package/src/dap/provider.ts +26 -0
- package/src/lsp/providers.ts +9 -5
- package/src/lsp/server.ts +26 -1
- package/src/rpc.ts +9 -0
- package/src/selfUpdate.ts +188 -0
- package/src/shared/rpc.ts +4 -0
package/src/lsp/server.ts
CHANGED
|
@@ -8,7 +8,8 @@ import { spawnServer, type Transport } from './transport.js'
|
|
|
8
8
|
import { createLspClient, type LspClient } from './client.js'
|
|
9
9
|
import { pathToFileUri, toWorkspacePath, isAbsolutePath, fileUriToPath } from './uri.js'
|
|
10
10
|
import { deriveDefinitionFromLocations, deriveCrossFileDeclaration, isMisresolvedSameFileUsage } from './derive.js'
|
|
11
|
-
import { readFile } from 'node:fs/promises'
|
|
11
|
+
import { readFile, readdir } from 'node:fs/promises'
|
|
12
|
+
import { join } from 'node:path'
|
|
12
13
|
import type { LspProviderSpec } from './providers.js'
|
|
13
14
|
import { LSP_SEMANTIC_TOKEN_MODIFIERS, LSP_SEMANTIC_TOKEN_TYPES } from '../shared/lsp.js'
|
|
14
15
|
import type {
|
|
@@ -61,6 +62,26 @@ interface OpenDoc {
|
|
|
61
62
|
const RESTARTABLE = true
|
|
62
63
|
const MAX_DOC_VERSION = 2147483647
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* 找工作区根目录下的第一个 .sln(字典序)。
|
|
67
|
+
*
|
|
68
|
+
* 背景:Roslyn LSP 启动后若收不到 solution/project 通知,会停留在「杂项文件」模式——
|
|
69
|
+
* 工作区内自有符号可解析,但 Unity 引擎程序集 / NuGet 包等元数据符号的
|
|
70
|
+
* definition/hover 一律返回空(实测 2026-10-16,GameObject.Find 全空)。
|
|
71
|
+
* 需在握手后发 Roslyn 专有 `solution/open` 通知加载项目上下文。
|
|
72
|
+
* @author ddj 2026年10月19号
|
|
73
|
+
* @param root 工作区根(绝对路径)
|
|
74
|
+
* @returns sln 绝对路径;根目录无 sln 返回 null
|
|
75
|
+
*/
|
|
76
|
+
async function findSolutionFile(root: string): Promise<string | null> {
|
|
77
|
+
try {
|
|
78
|
+
const names = (await readdir(root)).filter((name) => name.toLowerCase().endsWith('.sln')).sort()
|
|
79
|
+
return names[0] ? join(root, names[0]) : null
|
|
80
|
+
} catch {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
64
85
|
/**
|
|
65
86
|
* 使用合法、递增的 Int32 文档版本;旧客户端的毫秒时间戳不能直发给 Roslyn。
|
|
66
87
|
* @author ddj 2026年09月24号
|
|
@@ -320,6 +341,10 @@ export function createLspServer(spec: LspProviderSpec, root: string, languageId:
|
|
|
320
341
|
// 对不认识的配置段按协议忽略,均安全
|
|
321
342
|
if (languageId === 'csharp') {
|
|
322
343
|
client.notify('workspace/didChangeConfiguration', { settings: { dotrush: { roslyn: {} } } })
|
|
344
|
+
// Roslyn 专有:加载解决方案后元数据符号(引擎/NuGet 程序集)才可跳转;
|
|
345
|
+
// 未知通知对其它 C# 服务器按协议安全忽略。无 sln 的纯脚本项目静默跳过。
|
|
346
|
+
const slnPath = await findSolutionFile(root)
|
|
347
|
+
if (slnPath) client.notify('solution/open', { solution: pathToFileUri(slnPath) })
|
|
323
348
|
}
|
|
324
349
|
capabilities = caps
|
|
325
350
|
return true
|
package/src/rpc.ts
CHANGED
|
@@ -42,6 +42,7 @@ import { unityAdd, unityInstall, unityList, unityRemove } from './unityBridge.js
|
|
|
42
42
|
import { ackPending, handoffOpen, pendingState, pollPending } from './externalHandoff.js'
|
|
43
43
|
import { buildReport } from './compat.js'
|
|
44
44
|
import { findProfileDir, readDevForm, setDevForm } from './devForm.js'
|
|
45
|
+
import { checkUpdate, startSelfUpdate } from './selfUpdate.js'
|
|
45
46
|
import { normalizeRel } from './tree.js'
|
|
46
47
|
import { baseNameOf, checkNewName, checkRenameName, isSubPath, joinRelPath, parentRelOf } from './shared/fsNames.js'
|
|
47
48
|
import { invalidateIndex, listDirCached } from './treeIndex.js'
|
|
@@ -1244,6 +1245,14 @@ export function buildHandlers(
|
|
|
1244
1245
|
return { ok: false, error: String(error) }
|
|
1245
1246
|
}
|
|
1246
1247
|
},
|
|
1248
|
+
'vscode.updateCheck': async () => {
|
|
1249
|
+
try { return await checkUpdate(ctx) }
|
|
1250
|
+
catch (error) { return { ok: false, error: String(error) } }
|
|
1251
|
+
},
|
|
1252
|
+
'vscode.updateSelf': async () => {
|
|
1253
|
+
try { return await startSelfUpdate(ctx) }
|
|
1254
|
+
catch (error) { return { ok: false, error: String(error) } }
|
|
1255
|
+
},
|
|
1247
1256
|
'edrv.integration.status': async () => {
|
|
1248
1257
|
try {
|
|
1249
1258
|
return { ok: true, ...(await shellMenuStatus(ctx, await integrationBaseUrlOf(ctx))) }
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — 自我更新:registry 最新版本检查与官方 pluginManager「先删后装」编排。
|
|
3
|
+
* 职责:compareVersion 语义化版本比较(纯函数可单测)、latestVersionOf 经 subprocess 运行
|
|
4
|
+
* `pnpm view <pkg> version --json`(与 installBundle 同一套 pnpm/registry 配置)、
|
|
5
|
+
* startSelfUpdate 把 removeBundle→installBundle 编排放进宿主级 setTimeout(先回 RPC 响应,
|
|
6
|
+
* 定时器回调不受本插件卸载影响,规避「删除自身后 install 不再执行」的自愈陷阱)。
|
|
7
|
+
* 注意:不 import @deepseek-ai/dsh-plugin-manager(鸭子类型引用 ctx.get('pluginManager')),
|
|
8
|
+
* 老宿主缺失该服务时返回明确错误引导官方插件管理页。
|
|
9
|
+
* 作者 ddj 2026-10-16
|
|
10
|
+
*/
|
|
11
|
+
import { PLUGIN_NAME, pluginVersionOf } from './compat.js'
|
|
12
|
+
import { childEnv } from './childEnv.js'
|
|
13
|
+
import { findProfileDir } from './devForm.js'
|
|
14
|
+
import type { Ctx } from './store.js'
|
|
15
|
+
|
|
16
|
+
/** 官方 dsh profile-boot 提供的包管理器启动参数(与 devForm.ts ProfileContext 对齐)。 */
|
|
17
|
+
interface PackageManagerSpec {
|
|
18
|
+
command: string
|
|
19
|
+
args?: string[]
|
|
20
|
+
env?: NodeJS.ProcessEnv
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** subprocess.spawn 句柄的最小形状(对齐 devForm.ts runPnpmInstall 的用法)。 */
|
|
24
|
+
interface SpawnHandle {
|
|
25
|
+
done: Promise<{ exitCode: number | null }>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** pluginManager 服务的最小鸭子类型(仅本流程用到的方法)。 */
|
|
29
|
+
interface PluginManagerLike {
|
|
30
|
+
removeBundle(name: string): Promise<unknown>
|
|
31
|
+
installBundle(spec: string, options?: unknown): Promise<unknown>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 检查更新结果(RPC vscode.updateCheck 响应载荷)。 */
|
|
35
|
+
export type UpdateCheckResult =
|
|
36
|
+
| { ok: true; current: string; latest: string; hasUpdate: boolean }
|
|
37
|
+
| { ok: false; current: string; error: string }
|
|
38
|
+
|
|
39
|
+
/** 防并发重入标志:一旦启动编排,本次进程内不再接受第二次更新请求。 */
|
|
40
|
+
let updating = false
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 语义化版本比较:核心段按 `.` 数值比较、段数不足补 0;预发布版(-beta 等)低于同号正式版,
|
|
44
|
+
* 构建元数据(+)与预发布粒度不参与排序。
|
|
45
|
+
* @private
|
|
46
|
+
* @author ddj 2026年10月16号
|
|
47
|
+
* @param a 左版本号
|
|
48
|
+
* @param b 右版本号
|
|
49
|
+
* @returns a>b 为 1,a<b 为 -1,相等为 0
|
|
50
|
+
*/
|
|
51
|
+
export function compareVersion(a: string, b: string): number {
|
|
52
|
+
const parse = (text: string): { nums: number[]; pre: boolean } => {
|
|
53
|
+
const value = String(text ?? '').trim().replace(/^[^\d]*/, '')
|
|
54
|
+
const cut = value.search(/[-+]/)
|
|
55
|
+
const core = cut >= 0 ? value.slice(0, cut) : value
|
|
56
|
+
const nums = core.split('.').map((part) => {
|
|
57
|
+
const num = Number.parseInt(part, 10)
|
|
58
|
+
return Number.isFinite(num) ? num : 0
|
|
59
|
+
})
|
|
60
|
+
return { nums, pre: cut >= 0 && value[cut] === '-' }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const left = parse(a)
|
|
64
|
+
const right = parse(b)
|
|
65
|
+
const total = Math.max(left.nums.length, right.nums.length)
|
|
66
|
+
for (let index = 0; index < total; index += 1) {
|
|
67
|
+
const diff = (left.nums[index] ?? 0) - (right.nums[index] ?? 0)
|
|
68
|
+
if (diff !== 0) return diff > 0 ? 1 : -1
|
|
69
|
+
}
|
|
70
|
+
if (left.pre !== right.pre) return left.pre ? -1 : 1
|
|
71
|
+
return 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 用 profileContext.packageManager(缺省 pnpm)查询 npm registry 上本包最新版本。
|
|
76
|
+
* @private
|
|
77
|
+
* @author ddj 2026年10月16号
|
|
78
|
+
* @param ctx DSH host 上下文
|
|
79
|
+
* @returns registry 最新版本号
|
|
80
|
+
* @throws pnpm 不存在、退出非零或输出不可解析
|
|
81
|
+
*/
|
|
82
|
+
async function latestVersionOf(ctx: Ctx): Promise<string> {
|
|
83
|
+
const dir = findProfileDir(ctx)
|
|
84
|
+
if (!dir) throw new Error('无法定位当前 profile,无法查询最新版本')
|
|
85
|
+
const runner = (ctx.get('profileContext') as { packageManager?: PackageManagerSpec } | undefined)?.packageManager
|
|
86
|
+
const text = await new Promise<string>((resolvePromise, rejectPromise) => {
|
|
87
|
+
let spawned = false
|
|
88
|
+
try {
|
|
89
|
+
const handle = ctx.get('subprocess').spawn({
|
|
90
|
+
argv: [runner?.command ?? 'pnpm', ...runner?.args ?? [], 'view', PLUGIN_NAME, 'version', '--json'],
|
|
91
|
+
cwd: dir,
|
|
92
|
+
env: childEnv(runner?.env),
|
|
93
|
+
stdio: { stdout: 'pipe', stderr: 'pipe', stdin: 'ignore' },
|
|
94
|
+
graceMs: 20000,
|
|
95
|
+
}) as SpawnHandle
|
|
96
|
+
spawned = true
|
|
97
|
+
readSpawnText(handle, resolvePromise, rejectPromise)
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (!spawned) rejectPromise(error instanceof Error ? error : new Error(String(error)))
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
return parseVersionJson(text)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 读取 spawn 句柄的 stdout 文本并等待退出(非零退出或空输出抛错)。
|
|
107
|
+
* @private
|
|
108
|
+
* @author ddj 2026年10月16号
|
|
109
|
+
* @param handle subprocess.spawn 返回句柄
|
|
110
|
+
* @param resolvePromise 成功回传 stdout 全文
|
|
111
|
+
* @param rejectPromise 失败回传错误
|
|
112
|
+
*/
|
|
113
|
+
function readSpawnText(
|
|
114
|
+
handle: SpawnHandle & { collected?: { stdout?: { readFrom(offset: number): { text: string; lossy?: boolean } } } },
|
|
115
|
+
resolvePromise: (text: string) => void,
|
|
116
|
+
rejectPromise: (error: Error) => void,
|
|
117
|
+
): void {
|
|
118
|
+
void handle.done.then((outcome) => {
|
|
119
|
+
const text = handle.collected?.stdout?.readFrom(0)?.text ?? ''
|
|
120
|
+
if (outcome?.exitCode !== 0 || !text.trim()) {
|
|
121
|
+
rejectPromise(new Error('pnpm view 失败(exit ' + String(outcome?.exitCode) + '):' + text.slice(0, 200)))
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
resolvePromise(text)
|
|
125
|
+
}, rejectPromise)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 解析 `pnpm view --json` 输出:JSON 字符串("1.2.3")或直接版本字面量均接受。
|
|
130
|
+
* @private
|
|
131
|
+
* @author ddj 2026年10月16号
|
|
132
|
+
* @param text stdout 全文
|
|
133
|
+
* @returns 版本号
|
|
134
|
+
* @throws 输出不含可识别版本
|
|
135
|
+
*/
|
|
136
|
+
function parseVersionJson(text: string): string {
|
|
137
|
+
const trimmed = text.trim()
|
|
138
|
+
try {
|
|
139
|
+
const value = JSON.parse(trimmed) as unknown
|
|
140
|
+
if (typeof value === 'string' && value.trim()) return value.trim()
|
|
141
|
+
} catch { /* 落到字面量兜底 */ }
|
|
142
|
+
if (/^\d+(\.\d+){0,3}([-+].*)?$/.test(trimmed)) return trimmed
|
|
143
|
+
throw new Error('无法解析 pnpm view 输出:' + trimmed.slice(0, 120))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 检查 registry 是否有比当前安装版本更新的版本。
|
|
148
|
+
* @public
|
|
149
|
+
* @author ddj 2026年10月16号
|
|
150
|
+
* @param ctx DSH host 上下文
|
|
151
|
+
* @returns 当前版本 / 最新版本 / 是否有更新
|
|
152
|
+
*/
|
|
153
|
+
export async function checkUpdate(ctx: Ctx): Promise<UpdateCheckResult> {
|
|
154
|
+
const current = pluginVersionOf()
|
|
155
|
+
try {
|
|
156
|
+
const latest = await latestVersionOf(ctx)
|
|
157
|
+
return { ok: true, current, latest, hasUpdate: latest !== '' && compareVersion(latest, current) > 0 }
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return { ok: false, current, error: String(error) }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 启动「先删后装」自我更新编排:校验通过后把编排放进宿主级 setTimeout(延迟 1s 让 RPC 响应
|
|
165
|
+
* 先送达客户端),立即返回 started;定时器回调不受本插件卸载影响。install 失败时
|
|
166
|
+
* pluginManager 会恢复 profile 的 package.json/pnpm-lock.yaml,插件保持未安装状态,
|
|
167
|
+
* 由用户在官方插件管理页重装恢复。
|
|
168
|
+
* @public
|
|
169
|
+
* @author ddj 2026年10月16号
|
|
170
|
+
* @param ctx DSH host 上下文
|
|
171
|
+
* @returns started 是否已启动编排;失败时 ok=false
|
|
172
|
+
* @throws 无(失败走返回值 error)
|
|
173
|
+
*/
|
|
174
|
+
export async function startSelfUpdate(ctx: Ctx): Promise<{ ok: true; started: true } | { ok: false; error: string }> {
|
|
175
|
+
if (updating) return { ok: false, error: '更新流程已在进行中,请重启 DSH 后观察结果' }
|
|
176
|
+
const manager = ctx.get('pluginManager') as PluginManagerLike | undefined
|
|
177
|
+
if (!manager || typeof manager.removeBundle !== 'function' || typeof manager.installBundle !== 'function') {
|
|
178
|
+
return { ok: false, error: '当前宿主无 pluginManager 服务,请在官方插件管理页卸载后重新安装以升级' }
|
|
179
|
+
}
|
|
180
|
+
updating = true
|
|
181
|
+
// 延迟启动:RPC 响应先送达客户端、UI 有机会展示「更新已开始」,再动 profile
|
|
182
|
+
setTimeout(() => {
|
|
183
|
+
manager.removeBundle(PLUGIN_NAME)
|
|
184
|
+
.then(() => manager.installBundle(PLUGIN_NAME))
|
|
185
|
+
.catch(() => { /* 编排已脱离本插件生命周期:失败状态由 pluginManager 恢复,无上报通道 */ })
|
|
186
|
+
}, 1000)
|
|
187
|
+
return { ok: true, started: true }
|
|
188
|
+
}
|
package/src/shared/rpc.ts
CHANGED
|
@@ -425,6 +425,8 @@ export interface RpcRequestMap {
|
|
|
425
425
|
'edrv.external.pendingState': { token: string; take?: boolean }
|
|
426
426
|
'vscode.devFormGet': {}
|
|
427
427
|
'vscode.devFormSet': { enabled: boolean; path?: string }
|
|
428
|
+
'vscode.updateCheck': {}
|
|
429
|
+
'vscode.updateSelf': {}
|
|
428
430
|
'compat': {}
|
|
429
431
|
'edrv.lsp.status': { sessionId?: string }
|
|
430
432
|
'edrv.lsp.detect': {}
|
|
@@ -589,6 +591,8 @@ export interface RpcOkMap {
|
|
|
589
591
|
'edrv.external.pendingState': { delivered: boolean; completed?: boolean; error?: string }
|
|
590
592
|
'vscode.devFormGet': { devForm: DevFormInfo }
|
|
591
593
|
'vscode.devFormSet': { devForm: DevFormInfo; restart: boolean }
|
|
594
|
+
'vscode.updateCheck': { ok: boolean; current?: string; latest?: string; hasUpdate?: boolean; error?: string }
|
|
595
|
+
'vscode.updateSelf': { ok: boolean; started?: boolean; error?: string }
|
|
592
596
|
'compat': { report: CompatReport }
|
|
593
597
|
'edrv.lsp.status': { servers: LspServerStatus[] }
|
|
594
598
|
'edrv.lsp.detect': { servers: LspServerStatus[] }
|