dsh-vscode-mode 0.1.45 → 0.1.47
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 +4 -0
- package/lib/client.js +256 -8
- package/lib/client.js.map +1 -1
- package/lib/index.js +590 -34
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/monaco/lsp/providers.ts +10 -1
- package/src/client/monaco/lsp/underline.ts +101 -0
- package/src/client/styles/editor.css +2 -0
- package/src/client/ui/EditorView.ts +2 -0
- package/src/client/ui/LspSettings.ts +83 -5
- package/src/lsp/client.ts +4 -0
- package/src/lsp/config.ts +5 -1
- package/src/lsp/derive.ts +96 -0
- package/src/lsp/dotnetProvision.ts +224 -0
- package/src/lsp/envRequirements.ts +92 -0
- package/src/lsp/providers.ts +198 -25
- package/src/lsp/rpc.ts +24 -1
- package/src/lsp/server.ts +58 -5
- package/src/shared/lsp.ts +75 -0
- package/src/shared/rpc.ts +5 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — LSP 环境需求检测注册表。
|
|
3
|
+
* 每个语言注册一个检测器:扫描当前环境,返回未满足的需求清单(含一键安装与官网链接信息)。
|
|
4
|
+
* 新接入 LSP:在 CHECKERS 加一个检测器条目;需要内置安装器时在 INSTALLERS 注册同前缀安装函数。
|
|
5
|
+
* 检测在 status/redetect/configUpdate 时执行(不缓存),保证安装完成后状态即时刷新。
|
|
6
|
+
* 作者 ddj 2026年09月03号
|
|
7
|
+
*/
|
|
8
|
+
import { dirname } from 'node:path'
|
|
9
|
+
import { candidateCSharpServers, dotnetWithRuntime, runtimeMajorOf } from './providers.js'
|
|
10
|
+
import { startEnvInstall } from './dotnetProvision.js'
|
|
11
|
+
import type { LspMissingEnv } from '../shared/lsp.js'
|
|
12
|
+
|
|
13
|
+
/** dotnet 运行时需求构造(DotRush / ms-Roslyn 共用)。 */
|
|
14
|
+
function dotnetRuntimeRequirement(major: number, detail: string): LspMissingEnv {
|
|
15
|
+
return {
|
|
16
|
+
id: 'dotnet-runtime:' + major,
|
|
17
|
+
label: '.NET 运行时 ' + major + '.0',
|
|
18
|
+
detail,
|
|
19
|
+
installable: true,
|
|
20
|
+
manualUrl: 'https://dotnet.microsoft.com/download/dotnet/' + major + '.0',
|
|
21
|
+
manualLabel: '官网下载',
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** csharp 检测器:DotRush 缺运行时 / ms-Roslyn 缺 dotnet。 */
|
|
26
|
+
function checkCSharp(home: string): LspMissingEnv[] {
|
|
27
|
+
const found = candidateCSharpServers(home)
|
|
28
|
+
const missing: LspMissingEnv[] = []
|
|
29
|
+
if (found.dotrushDll) {
|
|
30
|
+
const major = runtimeMajorOf(dirname(found.dotrushDll))
|
|
31
|
+
if (major !== null && dotnetWithRuntime(major) === null) {
|
|
32
|
+
missing.push(dotnetRuntimeRequirement(major, 'DotRush 语言服务器运行所需'))
|
|
33
|
+
}
|
|
34
|
+
return missing
|
|
35
|
+
}
|
|
36
|
+
if (found.roslynDll && !found.dotnet) {
|
|
37
|
+
// 官方 Roslyn 服务器分支未实测,保守按 LTS 8 基线提示
|
|
38
|
+
missing.push(dotnetRuntimeRequirement(8, 'Roslyn 语言服务器运行所需'))
|
|
39
|
+
}
|
|
40
|
+
return missing
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** lua 检测器:EmmyLua/LuaLS 自带运行时,无外部依赖。 */
|
|
44
|
+
function checkLua(): LspMissingEnv[] {
|
|
45
|
+
return []
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 检测器注册表(按 languageId;新 LSP 在此加条目即可)。 */
|
|
49
|
+
const CHECKERS: Record<string, (home: string) => LspMissingEnv[]> = {
|
|
50
|
+
lua: checkLua,
|
|
51
|
+
csharp: checkCSharp,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 安装器注册表(按需求 id 前缀匹配;新依赖类型在此加条目)。 */
|
|
55
|
+
const INSTALLERS: { prefix: string; run: (id: string) => boolean }[] = [
|
|
56
|
+
{
|
|
57
|
+
prefix: 'dotnet-runtime:',
|
|
58
|
+
run: (id) => {
|
|
59
|
+
const major = Number.parseInt(id.slice('dotnet-runtime:'.length), 10)
|
|
60
|
+
return Number.isInteger(major) ? startEnvInstall(major) : false
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 检测某语言未满足的环境需求(仅返回缺失项;检测器异常按无需求处理)。
|
|
67
|
+
* @author ddj 2026年09月03号
|
|
68
|
+
* @param languageId 语言 id
|
|
69
|
+
* @param home DSH home(测试可注入临时目录)
|
|
70
|
+
* @returns 缺失需求清单
|
|
71
|
+
*/
|
|
72
|
+
export function envRequirementsFor(languageId: string, home: string): LspMissingEnv[] {
|
|
73
|
+
const checker = CHECKERS[languageId]
|
|
74
|
+
if (!checker) return []
|
|
75
|
+
try {
|
|
76
|
+
return checker(home)
|
|
77
|
+
} catch {
|
|
78
|
+
return []
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 尝试一键安装某条需求(存在内置安装器时启动,进行中/已满足返回 false)。
|
|
84
|
+
* @author ddj 2026年09月03号
|
|
85
|
+
* @param id 需求标识(如 dotnet-runtime:10)
|
|
86
|
+
* @returns 是否成功启动安装
|
|
87
|
+
*/
|
|
88
|
+
export function installRequirement(id: string): boolean {
|
|
89
|
+
const installer = INSTALLERS.find((it) => id.startsWith(it.prefix))
|
|
90
|
+
if (!installer) return false
|
|
91
|
+
return installer.run(id)
|
|
92
|
+
}
|
package/src/lsp/providers.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode host — 语言服务器 provider 解析。
|
|
3
3
|
* 优先级:手动配置 > extmgr 扩展源(Phase 2 挂载) > 自动发现(~/.vscode/extensions、
|
|
4
|
-
* ~/.dsh/dsh-vscode-mode/extensions、PATH)
|
|
5
|
-
*
|
|
4
|
+
* ~/.dsh/dsh-vscode-mode/extensions、PATH)。C# 自动发现覆盖 ms-dotnettools.csharp
|
|
5
|
+
* (Roslyn/OmniSharp)与 DotRush(自带 DotRush.dll 服务器,需对应 .NET 运行时)。
|
|
6
|
+
* 产物 LspProviderSpec:{ ready, argv, cwd, reason },供 manager 启动服务器。
|
|
6
7
|
* 作者 ddj 2026-08-27
|
|
7
8
|
*/
|
|
8
9
|
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, rmSync, type Dirent } from 'node:fs'
|
|
@@ -20,6 +21,9 @@ export interface LspProviderSpec {
|
|
|
20
21
|
kind: LspProviderKind
|
|
21
22
|
argv: string[]
|
|
22
23
|
cwd?: string
|
|
24
|
+
env?: Record<string, string>
|
|
25
|
+
/** 需自动配置的 .NET 运行时大版本(发现 DotRush 但缺运行时时设置,见 dotnetProvision.ts)。 */
|
|
26
|
+
provisionRuntime?: number
|
|
23
27
|
ready: boolean
|
|
24
28
|
reason?: string
|
|
25
29
|
version?: string
|
|
@@ -56,8 +60,16 @@ export function listMatchingDirs(dir: string, prefix: string): string[] {
|
|
|
56
60
|
return entries.filter((e) => e.isDirectory() && e.name.startsWith(prefix)).map((e) => join(dir, e.name))
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* VSCode 扩展目录(~/.vscode/extensions、~/.vscode-server/extensions + extmgr 目录;无则空数组)。
|
|
65
|
+
* 测试可用 DSH_LSP_EXT_DIRS(平台路径分隔符分隔)完全覆盖扫描列表,避免受真实机器扩展影响。
|
|
66
|
+
* @author ddj 2026年09月03号
|
|
67
|
+
*/
|
|
60
68
|
export function vscodeExtensionsDirs(home = dshHome()): string[] {
|
|
69
|
+
const override = process.env.DSH_LSP_EXT_DIRS
|
|
70
|
+
if (override) {
|
|
71
|
+
return override.split(sep === '\\' ? ';' : ':').map((dir) => dir.trim()).filter((dir) => dir && existsSync(dir))
|
|
72
|
+
}
|
|
61
73
|
const userHome = homedir()
|
|
62
74
|
return [
|
|
63
75
|
join(userHome, '.vscode', 'extensions'),
|
|
@@ -90,7 +102,7 @@ export function registerExtensionProvider(languageId: string, argv: string[], cw
|
|
|
90
102
|
export function extensionProviderFor(languageId: string): LspProviderSpec | null {
|
|
91
103
|
const p = extProviders.get(languageId)
|
|
92
104
|
if (!p || !p.argv.length) return null
|
|
93
|
-
return { languageId, kind: 'extension', argv: p.argv, cwd: p.cwd, ready: true, version: p.version, providerName:
|
|
105
|
+
return { languageId, kind: 'extension', argv: p.argv, cwd: p.cwd, ready: true, version: p.version, providerName: '语言服务器扩展' }
|
|
94
106
|
}
|
|
95
107
|
|
|
96
108
|
/** 清空全部扩展源注册(插件卸载/扩展目录重建时)。 */
|
|
@@ -113,18 +125,104 @@ export function findInPath(name: string): string | null {
|
|
|
113
125
|
return null
|
|
114
126
|
}
|
|
115
127
|
|
|
116
|
-
/**
|
|
117
|
-
export function
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
128
|
+
/** dotnet 候选(PATH 命中 + 常见安装路径 + 用户级安装位置),仅保留真实存在者。 */
|
|
129
|
+
export function dotnetCandidates(): string[] {
|
|
130
|
+
const localAppData = process.env.LOCALAPPDATA
|
|
131
|
+
const out = [
|
|
132
|
+
findInPath('dotnet'),
|
|
121
133
|
'C:/Program Files/dotnet/dotnet.exe',
|
|
122
134
|
'C:/Program Files (x86)/dotnet/dotnet.exe',
|
|
135
|
+
localAppData ? join(localAppData, 'Microsoft', 'dotnet', 'dotnet.exe') : null,
|
|
123
136
|
]
|
|
124
|
-
|
|
137
|
+
return out.filter((p): p is string => !!p && existsSync(p))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 查找 dotnet(PATH + 常见安装路径)。 */
|
|
141
|
+
export function findDotnet(): string | null {
|
|
142
|
+
return dotnetCandidates()[0] ?? null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 查找具备指定 .NET 运行时大版本的 dotnet(按 dotnetCandidates 顺序探测)。
|
|
147
|
+
* @author ddj 2026年09月03号
|
|
148
|
+
* @param minMajor 运行时最低大版本(如 10)
|
|
149
|
+
* @param candidates dotnet 候选路径(缺省 dotnetCandidates();测试可注入)
|
|
150
|
+
* @returns 满足条件的 dotnet 路径;无满足者返回 null
|
|
151
|
+
*/
|
|
152
|
+
export function dotnetWithRuntime(minMajor: number, candidates: string[] = dotnetCandidates()): string | null {
|
|
153
|
+
for (const dotnet of candidates) {
|
|
154
|
+
const sharedDir = join(dirname(dotnet), 'shared', 'Microsoft.NETCore.App')
|
|
155
|
+
let versions: string[] = []
|
|
156
|
+
try {
|
|
157
|
+
versions = readdirSync(sharedDir)
|
|
158
|
+
} catch (error) {
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
const found = versions.some((name) => {
|
|
162
|
+
const major = Number.parseInt(name.split('.')[0] ?? '', 10)
|
|
163
|
+
return Number.isInteger(major) && major >= minMajor
|
|
164
|
+
})
|
|
165
|
+
if (found) return dotnet
|
|
166
|
+
}
|
|
167
|
+
return null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 探测含 .NET SDK 的 dotnet 根目录(MSBuild 工程求值与 restore 需要,runtime-only 不够)。
|
|
172
|
+
* @author ddj 2026年09月03号
|
|
173
|
+
* @param candidates dotnet 候选路径(缺省 dotnetCandidates();测试可注入)
|
|
174
|
+
* @returns 含 SDK 的 dotnet 根目录;无则 null
|
|
175
|
+
*/
|
|
176
|
+
export function findSdkRoot(candidates: string[] = dotnetCandidates()): string | null {
|
|
177
|
+
for (const dotnet of candidates) {
|
|
178
|
+
const root = dirname(dotnet)
|
|
179
|
+
if (sdkVersionOf(root)) return root
|
|
180
|
+
}
|
|
125
181
|
return null
|
|
126
182
|
}
|
|
127
183
|
|
|
184
|
+
/**
|
|
185
|
+
* dotnet 根目录下最新 SDK 版本号(无 SDK 返回 null)。
|
|
186
|
+
* @author ddj 2026年09月03号
|
|
187
|
+
* @param root dotnet 根目录
|
|
188
|
+
* @returns SDK 版本号
|
|
189
|
+
*/
|
|
190
|
+
export function sdkVersionOf(root: string): string | null {
|
|
191
|
+
try {
|
|
192
|
+
const versions = readdirSync(join(root, 'sdk')).filter((name) => /^\d+\.\d+/.test(name))
|
|
193
|
+
if (!versions.length) return null
|
|
194
|
+
return versions.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))[0] ?? null
|
|
195
|
+
} catch (error) {
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* C# 服务器所需的 MSBuild 环境变量(SDK 解析 + restore 的 muxer 定位)。
|
|
202
|
+
* @author ddj 2026年09月03号
|
|
203
|
+
* @param sdkRoot 含 SDK 的 dotnet 根目录
|
|
204
|
+
* @returns 环境变量表
|
|
205
|
+
*/
|
|
206
|
+
export function sdkEnvOf(sdkRoot: string): Record<string, string> {
|
|
207
|
+
const version = sdkVersionOf(sdkRoot)
|
|
208
|
+
const env: Record<string, string> = {
|
|
209
|
+
DOTNET_ROOT: sdkRoot,
|
|
210
|
+
DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR: sdkRoot,
|
|
211
|
+
}
|
|
212
|
+
if (version) env.DOTNET_SDK_PATH = join(sdkRoot, 'sdk', version)
|
|
213
|
+
return env
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 读扩展目录 package.json 的 version(读取失败不影响入口使用,返回 undefined)。 */
|
|
217
|
+
function manifestVersionOf(extDir: string): string | undefined {
|
|
218
|
+
try {
|
|
219
|
+
const manifest = JSON.parse(readFileSync(join(extDir, 'package.json'), 'utf8')) as { version?: unknown }
|
|
220
|
+
return typeof manifest.version === 'string' ? manifest.version : undefined
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return undefined
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
128
226
|
/** EmmyLua 自动发现候选(tangzx.emmylua-* / server/emmylua_ls)。 */
|
|
129
227
|
export function candidateEmmyLua(home = dshHome()): { path: string; version?: string } | null {
|
|
130
228
|
const candidates: { path: string; version?: string }[] = []
|
|
@@ -132,12 +230,7 @@ export function candidateEmmyLua(home = dshHome()): { path: string; version?: st
|
|
|
132
230
|
for (const extDir of listMatchingDirs(dir, 'tangzx.emmylua-')) {
|
|
133
231
|
const server = join(extDir, 'server', 'emmylua_ls' + exeSuffix())
|
|
134
232
|
if (!existsSync(server)) continue
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const manifest = JSON.parse(readFileSync(join(extDir, 'package.json'), 'utf8')) as { version?: unknown }
|
|
138
|
-
if (typeof manifest.version === 'string') version = manifest.version
|
|
139
|
-
} catch (error) { /* 清单读取失败不影响入口使用 */ }
|
|
140
|
-
candidates.push({ path: server, version })
|
|
233
|
+
candidates.push({ path: server, version: manifestVersionOf(extDir) })
|
|
141
234
|
}
|
|
142
235
|
}
|
|
143
236
|
candidates.sort((a, b) => (b.version ?? '').localeCompare(a.version ?? '', undefined, { numeric: true }) || a.path.localeCompare(b.path))
|
|
@@ -161,9 +254,25 @@ export function candidateLuaServers(home = dshHome()): string[] {
|
|
|
161
254
|
return out
|
|
162
255
|
}
|
|
163
256
|
|
|
164
|
-
/** C#
|
|
165
|
-
export
|
|
166
|
-
|
|
257
|
+
/** C# 服务器发现结果(ms-Roslyn / DotRush / OmniSharp + 系统 dotnet)。 */
|
|
258
|
+
export interface CSharpCandidates {
|
|
259
|
+
roslynDll?: string
|
|
260
|
+
dotrushDll?: string
|
|
261
|
+
dotrushVersion?: string
|
|
262
|
+
omnisharpExe?: string
|
|
263
|
+
dotnet?: string
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* C# 自动发现候选:ms-dotnettools.csharp 的 Roslyn dll(需 dotnet)/ OmniSharp 可执行,
|
|
268
|
+
* 以及 DotRush 扩展自带服务器(extension/bin/LanguageServer/DotRush.dll,需对应 .NET 运行时)。
|
|
269
|
+
* @author ddj 2026年09月03号
|
|
270
|
+
* @param home DSH home(缺省真实;测试可注入临时目录)
|
|
271
|
+
* @returns 发现结果
|
|
272
|
+
*/
|
|
273
|
+
export function candidateCSharpServers(home = dshHome()): CSharpCandidates {
|
|
274
|
+
const out: CSharpCandidates = {}
|
|
275
|
+
const dotrushHits: { dll: string; version?: string }[] = []
|
|
167
276
|
for (const dir of vscodeExtensionsDirs(home)) {
|
|
168
277
|
for (const extDir of listMatchingDirs(dir, 'ms-dotnettools.csharp-')) {
|
|
169
278
|
const roslyn = join(extDir, '.roslyn', 'Microsoft.CodeAnalysis.LanguageServer.dll')
|
|
@@ -171,12 +280,57 @@ export function candidateCSharpServers(home = dshHome()): { roslynDll?: string;
|
|
|
171
280
|
const omni = join(extDir, '.omnisharp', 'OmniSharp' + exeSuffix())
|
|
172
281
|
if (!out.omnisharpExe && existsSync(omni)) out.omnisharpExe = omni
|
|
173
282
|
}
|
|
283
|
+
for (const extDir of listMatchingDirs(dir, 'nromanov.dotrush-')) {
|
|
284
|
+
const dll = join(extDir, 'extension', 'bin', 'LanguageServer', 'DotRush.dll')
|
|
285
|
+
if (!existsSync(dll)) continue
|
|
286
|
+
dotrushHits.push({ dll, version: manifestVersionOf(extDir) })
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// 多版本并存时取清单版本最高者(与 EmmyLua 发现的排序策略一致)
|
|
290
|
+
const best = dotrushHits.sort((a, b) => (b.version ?? '').localeCompare(a.version ?? '', undefined, { numeric: true }) || a.dll.localeCompare(b.dll))[0]
|
|
291
|
+
if (best) {
|
|
292
|
+
out.dotrushDll = best.dll
|
|
293
|
+
out.dotrushVersion = best.version
|
|
174
294
|
}
|
|
175
295
|
const dotnet = findDotnet()
|
|
176
296
|
if (dotnet) out.dotnet = dotnet
|
|
177
297
|
return out
|
|
178
298
|
}
|
|
179
299
|
|
|
300
|
+
/**
|
|
301
|
+
* 从服务器 dll 同目录的 *.runtimeconfig.json 解析所需 .NET 运行时大版本。
|
|
302
|
+
* @author ddj 2026年09月03号
|
|
303
|
+
* @param dllDir 服务器 dll 所在目录
|
|
304
|
+
* @returns 运行时大版本;无清单或解析失败返回 null
|
|
305
|
+
*/
|
|
306
|
+
export function runtimeMajorOf(dllDir: string): number | null {
|
|
307
|
+
try {
|
|
308
|
+
for (const name of readdirSync(dllDir)) {
|
|
309
|
+
if (!name.endsWith('.runtimeconfig.json')) continue
|
|
310
|
+
const config = JSON.parse(readFileSync(join(dllDir, name), 'utf8')) as { runtimeOptions?: { framework?: { version?: unknown } } }
|
|
311
|
+
const version = config.runtimeOptions?.framework?.version
|
|
312
|
+
if (typeof version !== 'string') continue
|
|
313
|
+
const major = Number.parseInt(version.split('.')[0] ?? '', 10)
|
|
314
|
+
if (Number.isInteger(major)) return major
|
|
315
|
+
}
|
|
316
|
+
} catch (error) { /* 清单读取失败按无约束处理 */ }
|
|
317
|
+
return null
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* 解析 DotRush 启动参数:优先扩展内置 SDK dotnet,否则找满足其运行时大版本的 dotnet。
|
|
322
|
+
* @author ddj 2026年09月03号
|
|
323
|
+
* @param dll DotRush.dll 绝对路径
|
|
324
|
+
* @returns argv 可直接启动;reason 为不可启动的原因
|
|
325
|
+
*/
|
|
326
|
+
function dotrushLaunch(dll: string): { argv: string[] } | { reason: string } {
|
|
327
|
+
const sdkDotnet = join(dirname(dirname(dll)), 'Sdk', 'dotnet' + exeSuffix())
|
|
328
|
+
const major = runtimeMajorOf(dirname(dll))
|
|
329
|
+
const dotnet = existsSync(sdkDotnet) ? sdkDotnet : major !== null ? dotnetWithRuntime(major) : findDotnet()
|
|
330
|
+
if (dotnet) return { argv: [dotnet, dll] }
|
|
331
|
+
return { reason: 'DotRush 需 .NET ' + (major ?? '') + ' 运行时,未找到满足的 dotnet' }
|
|
332
|
+
}
|
|
333
|
+
|
|
180
334
|
/**
|
|
181
335
|
* 手动配置归一化:path 指向可执行文件/目录,command 为可执行名(PATH 解析)。
|
|
182
336
|
* 未配置时返回空 reason(由各 resolve 兜底输出语言相关安装指引文案)。
|
|
@@ -221,7 +375,8 @@ export function resolveLuaProvider(manual?: { command?: string; path?: string },
|
|
|
221
375
|
}
|
|
222
376
|
|
|
223
377
|
/**
|
|
224
|
-
* 解析 C# provider(优先级:手动 > 扩展源 >
|
|
378
|
+
* 解析 C# provider(优先级:手动 > 扩展源 > 自动发现;Roslyn dll 需 dotnet;
|
|
379
|
+
* DotRush 需满足其运行时的 dotnet;或 OmniSharp)。
|
|
225
380
|
* @author ddj 2026年08月27号
|
|
226
381
|
* @param manual 手动配置
|
|
227
382
|
* @param home DSH home(缺省真实;测试可注入临时目录)
|
|
@@ -235,16 +390,34 @@ export function resolveCSharpProvider(manual?: { command?: string; path?: string
|
|
|
235
390
|
const ext = extensionProviderFor('csharp')
|
|
236
391
|
if (ext) return ext
|
|
237
392
|
const discovered = candidateCSharpServers(h)
|
|
393
|
+
// SDK 环境注入:runtime-only 的 dotnet 会导致 MSBuild 求值/restore 退化
|
|
394
|
+
const sdkRoot = findSdkRoot()
|
|
395
|
+
const msbuildEnv = sdkRoot ? sdkEnvOf(sdkRoot) : undefined
|
|
238
396
|
if (discovered.roslynDll && discovered.dotnet) {
|
|
239
|
-
return { ...base, kind: 'discover', argv: [discovered.dotnet, discovered.roslynDll, '--stdio'], ready: true }
|
|
397
|
+
return { ...base, kind: 'discover', argv: [discovered.dotnet, discovered.roslynDll, '--stdio'], env: msbuildEnv, ready: true }
|
|
398
|
+
}
|
|
399
|
+
let dotrushReason: string | null = null
|
|
400
|
+
let provisionMajor: number | undefined
|
|
401
|
+
if (discovered.dotrushDll) {
|
|
402
|
+
const major = runtimeMajorOf(dirname(discovered.dotrushDll))
|
|
403
|
+
const launch = dotrushLaunch(discovered.dotrushDll)
|
|
404
|
+
if ('argv' in launch) {
|
|
405
|
+
return { ...base, kind: 'discover', argv: launch.argv, env: msbuildEnv, ready: true, version: discovered.dotrushVersion, providerName: 'DotRush' }
|
|
406
|
+
}
|
|
407
|
+
dotrushReason = launch.reason
|
|
408
|
+
provisionMajor = major ?? undefined
|
|
240
409
|
}
|
|
241
410
|
if (discovered.omnisharpExe) {
|
|
242
411
|
return { ...base, kind: 'discover', argv: [discovered.omnisharpExe, '--stdio'], ready: true }
|
|
243
412
|
}
|
|
244
413
|
const reasons: string[] = []
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
-
|
|
414
|
+
if (dotrushReason) {
|
|
415
|
+
reasons.push(dotrushReason)
|
|
416
|
+
} else {
|
|
417
|
+
if (!discovered.dotnet) reasons.push('未找到 dotnet')
|
|
418
|
+
reasons.push('未发现 ms-dotnettools.csharp / DotRush 扩展')
|
|
419
|
+
}
|
|
420
|
+
return { ...base, kind: 'none', argv: [], ready: false, provisionRuntime: provisionMajor, reason: reasons.join(';') + '(可手动指定 Roslyn/OmniSharp 入口)' }
|
|
248
421
|
})
|
|
249
422
|
}
|
|
250
423
|
|
|
@@ -319,7 +492,7 @@ function fingerprintOf(home: string): string {
|
|
|
319
492
|
return '?'
|
|
320
493
|
}
|
|
321
494
|
})
|
|
322
|
-
return process.platform + '|' + marks.join('|')
|
|
495
|
+
return process.platform + '|' + (findSdkRoot() ?? 'no-sdk') + '|' + marks.join('|')
|
|
323
496
|
}
|
|
324
497
|
|
|
325
498
|
/**
|
package/src/lsp/rpc.ts
CHANGED
|
@@ -8,10 +8,13 @@ import { searchRoot } from '../search/ripgrep.js'
|
|
|
8
8
|
import { sessionOf, cwdOf } from '../registry.js'
|
|
9
9
|
import { resolveProviderSpec, langOfPath, configFromPlugin, configFromSettings, LSP_LANGUAGES, LSP_SETTINGS_NS, type LspConfig } from './config.js'
|
|
10
10
|
import { clearProviderCache } from './providers.js'
|
|
11
|
+
import { onRuntimeProvisioned, envInstallStates } from './dotnetProvision.js'
|
|
12
|
+
import { envRequirementsFor, installRequirement } from './envRequirements.js'
|
|
11
13
|
import type { LspManager } from './manager.js'
|
|
12
14
|
import type { RpcHandlerMap } from '../shared/rpc.js'
|
|
13
15
|
import type { LspLocation, LspServerStatus } from '../shared/lsp.js'
|
|
14
16
|
import { installFromMarket, installVsixFile, listInstalled, marketGet, marketSearch, uninstall, updateExtension, type ExtInfo } from './extmgr.js'
|
|
17
|
+
import { dshHome } from '../paths.js'
|
|
15
18
|
|
|
16
19
|
export interface LspRpcDeps {
|
|
17
20
|
ctx: Ctx
|
|
@@ -114,6 +117,12 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
|
|
|
114
117
|
const { ctx, pluginConfig, manager } = deps
|
|
115
118
|
const tracker = createDocTracker()
|
|
116
119
|
|
|
120
|
+
// 运行时自动配置成功:清 provider 缓存并重置 C# 服务器,下一次文档同步即用新运行时重启
|
|
121
|
+
onRuntimeProvisioned(() => {
|
|
122
|
+
clearProviderCache()
|
|
123
|
+
manager.resetLanguage('csharp')
|
|
124
|
+
})
|
|
125
|
+
|
|
117
126
|
/** 解析会话与工作区根(失败 → null + error)。 */
|
|
118
127
|
const rootOf = async (sessionId: string | undefined): Promise<{ root: string; sessionId: string } | { err: string }> => {
|
|
119
128
|
const session = sessionOf(ctx, sessionId)
|
|
@@ -138,9 +147,10 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
|
|
|
138
147
|
const rootLocations = (locations: LspLocation[], root: string): LspLocation[] =>
|
|
139
148
|
locations.map((location) => ({ ...location, root }))
|
|
140
149
|
|
|
141
|
-
/** 当前 provider 检测结论 → 设置页 idle 状态(不启动 server
|
|
150
|
+
/** 当前 provider 检测结论 → 设置页 idle 状态(不启动 server);附带未满足的环境需求。 */
|
|
142
151
|
const detectedStatus = (languageId: string, root?: string): LspServerStatus => {
|
|
143
152
|
const spec = resolveProviderSpec(ctx, pluginConfig, languageId)
|
|
153
|
+
const missingEnv = envRequirementsFor(languageId, dshHome())
|
|
144
154
|
return {
|
|
145
155
|
languageId,
|
|
146
156
|
source: spec.kind,
|
|
@@ -149,6 +159,7 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
|
|
|
149
159
|
version: spec.version,
|
|
150
160
|
providerName: spec.providerName,
|
|
151
161
|
root,
|
|
162
|
+
missingEnv: missingEnv.length ? missingEnv : undefined,
|
|
152
163
|
}
|
|
153
164
|
}
|
|
154
165
|
|
|
@@ -352,6 +363,18 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
|
|
|
352
363
|
}
|
|
353
364
|
},
|
|
354
365
|
|
|
366
|
+
// 环境需求:一键安装(内置安装器)与安装进度查询
|
|
367
|
+
'edrv.lsp.envInstall': async (args) => {
|
|
368
|
+
if (!installRequirement(args.id)) {
|
|
369
|
+
return { ok: false, started: false, error: '该需求不支持一键安装:' + args.id }
|
|
370
|
+
}
|
|
371
|
+
return { ok: true, started: true }
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
'edrv.lsp.envState': async () => {
|
|
375
|
+
return { ok: true, states: envInstallStates() }
|
|
376
|
+
},
|
|
377
|
+
|
|
355
378
|
// --region 扩展管理(edrv.lsp.ext.*:已装/市场/安装/卸载/更新)
|
|
356
379
|
'edrv.lsp.ext.list': async () => ({ ok: true, extensions: listInstalled() }),
|
|
357
380
|
|
package/src/lsp/server.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { spawnServer, type Transport } from './transport.js'
|
|
8
8
|
import { createLspClient, type LspClient } from './client.js'
|
|
9
9
|
import { pathToFileUri } from './uri.js'
|
|
10
|
+
import { deriveDefinitionFromLocations } from './derive.js'
|
|
10
11
|
import type { LspProviderSpec } from './providers.js'
|
|
11
12
|
import { LSP_SEMANTIC_TOKEN_MODIFIERS, LSP_SEMANTIC_TOKEN_TYPES } from '../shared/lsp.js'
|
|
12
13
|
import type {
|
|
@@ -64,6 +65,7 @@ export function createLspServer(spec: LspProviderSpec, root: string, languageId:
|
|
|
64
65
|
let client: LspClient | null = null
|
|
65
66
|
let capabilities: LspServerCapabilities = {
|
|
66
67
|
definition: false,
|
|
68
|
+
declaration: false,
|
|
67
69
|
references: false,
|
|
68
70
|
documentSymbol: false,
|
|
69
71
|
workspaceSymbol: false,
|
|
@@ -152,7 +154,7 @@ export function createLspServer(spec: LspProviderSpec, root: string, languageId:
|
|
|
152
154
|
setPhase('starting')
|
|
153
155
|
try {
|
|
154
156
|
transport = spawnServer(
|
|
155
|
-
{ argv: spec.argv, cwd: spec.cwd ?? root },
|
|
157
|
+
{ argv: spec.argv, cwd: spec.cwd ?? root, env: spec.env },
|
|
156
158
|
(message) => { /* 由 client 接管 */ },
|
|
157
159
|
(line) => log('stderr: ' + line),
|
|
158
160
|
)
|
|
@@ -185,6 +187,13 @@ export function createLspServer(spec: LspProviderSpec, root: string, languageId:
|
|
|
185
187
|
})
|
|
186
188
|
// 让 client 接管 stdout 消息
|
|
187
189
|
const caps = await client.initialize(rootUri, [rootUri])
|
|
190
|
+
// DotRush 服务器阻塞等待带 dotrush 段的配置通知才继续初始化(加载工作区),
|
|
191
|
+
// 不发则所有文档类查询返回空;官方 VSCode 扩展经 configurationSection 自动发送。
|
|
192
|
+
// 按 csharp 语言统一发送:手动配置的 DotRush 与其它 C# 服务器(OmniSharp 等)
|
|
193
|
+
// 对不认识的配置段按协议忽略,均安全
|
|
194
|
+
if (languageId === 'csharp') {
|
|
195
|
+
client.notify('workspace/didChangeConfiguration', { settings: { dotrush: { roslyn: {} } } })
|
|
196
|
+
}
|
|
188
197
|
capabilities = caps
|
|
189
198
|
return true
|
|
190
199
|
} catch (error) {
|
|
@@ -231,11 +240,55 @@ export function createLspServer(spec: LspProviderSpec, root: string, languageId:
|
|
|
231
240
|
async definition(path: string, line: number, character: number): Promise<LspLocation[]> {
|
|
232
241
|
const doc = docs.get(path)
|
|
233
242
|
if (!doc || !(await readyWait)) return []
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
243
|
+
const client = ensureClient()
|
|
244
|
+
const position = { line, character }
|
|
245
|
+
// ① 标准定义(方法/跨文件命名空间等)
|
|
246
|
+
const direct = normalizeLocations(
|
|
247
|
+
await client.request<unknown>(
|
|
248
|
+
'textDocument/definition',
|
|
249
|
+
{ textDocument: { uri: doc.uri }, position },
|
|
250
|
+
),
|
|
237
251
|
)
|
|
238
|
-
|
|
252
|
+
if (direct.length) return direct
|
|
253
|
+
// ② 声明(服务器能力支持时):EmmyLua 对局部变量/参数/表字段 definition 常返回空,declaration 更准
|
|
254
|
+
if (capabilities.declaration) {
|
|
255
|
+
try {
|
|
256
|
+
const decl = normalizeLocations(
|
|
257
|
+
await client.request<unknown>(
|
|
258
|
+
'textDocument/declaration',
|
|
259
|
+
{ textDocument: { uri: doc.uri }, position },
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
if (decl.length) {
|
|
263
|
+
log('definition fallback → declaration (' + path + ':' + line + ':' + character + ')')
|
|
264
|
+
return decl
|
|
265
|
+
}
|
|
266
|
+
} catch (error) {
|
|
267
|
+
log('declaration request failed: ' + String(error))
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// ③ 引用推导:definition/declaration 均空时,从 references(includeDeclaration) 推导声明
|
|
271
|
+
// (EmmyLua 局部/参数把声明排在引用首位;词文本不一致则拒绝,宁可空不误跳)
|
|
272
|
+
try {
|
|
273
|
+
const refs = normalizeLocations(
|
|
274
|
+
await client.request<unknown>(
|
|
275
|
+
'textDocument/references',
|
|
276
|
+
{
|
|
277
|
+
textDocument: { uri: doc.uri },
|
|
278
|
+
position,
|
|
279
|
+
context: { includeDeclaration: true },
|
|
280
|
+
},
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
const derived = deriveDefinitionFromLocations(position, refs, doc.text, doc.uri)
|
|
284
|
+
if (derived) {
|
|
285
|
+
log('definition fallback → references-derived (' + path + ':' + line + ':' + character + ')')
|
|
286
|
+
return [derived]
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
log('references fallback failed: ' + String(error))
|
|
290
|
+
}
|
|
291
|
+
return []
|
|
239
292
|
},
|
|
240
293
|
|
|
241
294
|
async references(path: string, line: number, character: number, includeDeclaration: boolean): Promise<LspLocation[]> {
|
package/src/shared/lsp.ts
CHANGED
|
@@ -69,9 +69,66 @@ export interface LspSemanticTokens {
|
|
|
69
69
|
resultId?: string
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/** 语义 token 解码后的绝对范围(LSP 0-based;type 为插件固定 legend 与 type 名)。 */
|
|
73
|
+
export interface LspSemanticTokenRange {
|
|
74
|
+
start: LspPosition
|
|
75
|
+
end: LspPosition
|
|
76
|
+
type: string
|
|
77
|
+
modifiers: number
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 语义 token 中可作为"可导航标识符"的类型(ctrl+hover 下划线判定用)。 */
|
|
81
|
+
export const LSP_NAVIGABLE_TOKEN_TYPES: ReadonlySet<string> = new Set([
|
|
82
|
+
'variable', 'parameter', 'property', 'function', 'method', 'class', 'enum',
|
|
83
|
+
'struct', 'interface', 'namespace', 'typeParameter', 'enumMember', 'event', 'type',
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 语义 token 是否属于可导航标识符。
|
|
88
|
+
* @author ddj 2026年09月02号
|
|
89
|
+
* @param type 语义 token 类型名(插件固定 legend)
|
|
90
|
+
* @returns 是否可导航
|
|
91
|
+
*/
|
|
92
|
+
export function isNavigableTokenType(type: string): boolean {
|
|
93
|
+
return LSP_NAVIGABLE_TOKEN_TYPES.has(type)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 解码 LSP delta 编码的语义 token 流为绝对范围数组。
|
|
98
|
+
* @author ddj 2026年09月02号
|
|
99
|
+
* @param data 5 元组 delta 流(host normalizeSemanticData 产物,类型索引对应固定 legend)
|
|
100
|
+
* @param types 固定 legend 类型表(缺省 LSP_SEMANTIC_TOKEN_TYPES)
|
|
101
|
+
* @returns 绝对范围列表(保持文档顺序)
|
|
102
|
+
*/
|
|
103
|
+
export function decodeSemanticTokens(
|
|
104
|
+
data: number[],
|
|
105
|
+
types: readonly string[] = LSP_SEMANTIC_TOKEN_TYPES,
|
|
106
|
+
): LspSemanticTokenRange[] {
|
|
107
|
+
const out: LspSemanticTokenRange[] = []
|
|
108
|
+
let line = 0
|
|
109
|
+
let character = 0
|
|
110
|
+
for (let i = 0; i + 4 < data.length; i += 5) {
|
|
111
|
+
const deltaLine = data[i] ?? 0
|
|
112
|
+
const deltaCharacter = data[i + 1] ?? 0
|
|
113
|
+
const length = Math.max(0, data[i + 2] ?? 0)
|
|
114
|
+
const typeIndex = data[i + 3] ?? 0
|
|
115
|
+
const modifiers = data[i + 4] ?? 0
|
|
116
|
+
line += deltaLine
|
|
117
|
+
character = deltaLine === 0 ? character + deltaCharacter : deltaCharacter
|
|
118
|
+
out.push({
|
|
119
|
+
start: { line, character },
|
|
120
|
+
end: { line, character: character + length },
|
|
121
|
+
type: types[typeIndex] ?? 'variable',
|
|
122
|
+
modifiers,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
return out
|
|
126
|
+
}
|
|
127
|
+
|
|
72
128
|
/** LSP 服务器能力子集:本插件会用到的方法。 */
|
|
73
129
|
export interface LspServerCapabilities {
|
|
74
130
|
definition: boolean
|
|
131
|
+
declaration: boolean
|
|
75
132
|
references: boolean
|
|
76
133
|
documentSymbol: boolean
|
|
77
134
|
workspaceSymbol: boolean
|
|
@@ -90,6 +147,23 @@ export type LspServerPhase =
|
|
|
90
147
|
| 'unavailable' // 连续失败,停止自动重启
|
|
91
148
|
| 'stopped' // 已停止(禁用/卸载/会话结束)
|
|
92
149
|
|
|
150
|
+
/** 单条缺失环境需求(edrv.lsp.status.missingEnv 元素,仅含未满足项)。 */
|
|
151
|
+
export interface LspMissingEnv {
|
|
152
|
+
id: string // 需求标识(如 dotnet-runtime:10,同时是安装器键)
|
|
153
|
+
label: string // 显示名(如 '.NET 运行时 10.0')
|
|
154
|
+
detail?: string // 说明(哪个服务器需要)
|
|
155
|
+
installable: boolean // 支持一键安装
|
|
156
|
+
manualUrl?: string // 官网手动下载地址
|
|
157
|
+
manualLabel?: string
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 环境安装进行态(edrv.lsp.envState 载荷元素)。 */
|
|
161
|
+
export interface LspEnvInstallState {
|
|
162
|
+
id: string
|
|
163
|
+
phase: 'idle' | 'downloading' | 'extracting' | 'done' | 'failed'
|
|
164
|
+
message?: string
|
|
165
|
+
}
|
|
166
|
+
|
|
93
167
|
/** 单语言服务器状态(edrv.lsp.status 载荷元素)。 */
|
|
94
168
|
export interface LspServerStatus {
|
|
95
169
|
languageId: string
|
|
@@ -101,6 +175,7 @@ export interface LspServerStatus {
|
|
|
101
175
|
progressMessage?: string
|
|
102
176
|
root?: string // 绑定工作区根(相对显示)
|
|
103
177
|
providerName?: string // provider 名称(如 EmmyLua / LuaLS)
|
|
178
|
+
missingEnv?: LspMissingEnv[] // 未满足的环境需求(无则省略)
|
|
104
179
|
}
|
|
105
180
|
|
|
106
181
|
/**
|