dsh-vscode-mode 0.1.50 → 0.1.52
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/lib/client.js +64 -27
- package/lib/client.js.map +1 -1
- package/lib/index.js +140 -100
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/ui/LspSettings.ts +56 -16
- package/src/index.ts +1 -2
- package/src/lsp/config.ts +13 -29
- package/src/lsp/providers.ts +76 -16
- package/src/lsp/rpc.ts +24 -20
- package/src/shared/lsp.ts +9 -0
- package/src/shared/rpc.ts +2 -0
- package/src/lsp/settings.ts +0 -50
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.52",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -2,17 +2,38 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* dsh-vscode-mode client — 「语言服务器」设置子 Tab。
|
|
4
4
|
* 四视图:服务器(状态 + 每语言启用/命令/路径)|已安装(卸载/更新)|市场(Open VSX 搜索/安装 + 本地 vsix)|更新。
|
|
5
|
-
* 状态经 edrv.lsp.
|
|
6
|
-
* 作者 ddj 2026-08-27
|
|
5
|
+
* 状态经 edrv.lsp.detect(检测结果驱动卡片动态显示),配置经 edrv.lsp.configGet/Update,扩展经 edrv.lsp.ext.*。
|
|
6
|
+
* 作者 ddj 2026-08-27 / 2026-09-03
|
|
7
7
|
*/
|
|
8
8
|
import React from 'react'
|
|
9
9
|
import { rpc } from '../rpc.js'
|
|
10
10
|
import '../styles/mcp.css'
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
/** 支持语言的展示元数据(卡片显示由检测结果驱动;未知 id 走 langMeta 兜底)。 */
|
|
13
|
+
const LANG_META = {
|
|
14
|
+
lua: { label: 'Lua(EmmyLua / LuaLS)', hint: '优先使用已安装 EmmyLua;未安装时回退 LuaLS 或 PATH 中的 lua-language-server' },
|
|
15
|
+
csharp: { label: 'C#(Roslyn / DotRush / OmniSharp)', hint: '优先自动发现 ms-dotnettools.csharp / DotRush 扩展(DotRush 需 .NET 10 运行时);也可手动指定' },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 语言展示元数据(含未知 id 兜底)。 */
|
|
19
|
+
function langMeta(id) {
|
|
20
|
+
return LANG_META[id] || { label: id + '(语言服务器)', hint: '' }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 该语言是否已存配置(禁用开关或手动命令/路径)。 */
|
|
24
|
+
function hasStoredConfig(cfg) {
|
|
25
|
+
if (!cfg || typeof cfg !== 'object') return false
|
|
26
|
+
if (cfg.enabled === false) return true
|
|
27
|
+
return Boolean((cfg.command && cfg.command.trim()) || (cfg.path && cfg.path.trim()))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 该语言是否显示卡片:检测到可用 LSP、已存配置、或存在待安装的缺失环境。 */
|
|
31
|
+
function langVisible(status, config) {
|
|
32
|
+
if (!status) return false
|
|
33
|
+
if (status.source && status.source !== 'none') return true
|
|
34
|
+
if (hasStoredConfig(config[status.languageId])) return true
|
|
35
|
+
return Array.isArray(status.missingEnv) && status.missingEnv.length > 0
|
|
36
|
+
}
|
|
16
37
|
|
|
17
38
|
const PHASE_LABEL = {
|
|
18
39
|
idle: '未启动', starting: '启动中', ready: '就绪', indexing: '索引中', unavailable: '不可用', stopped: '已停止',
|
|
@@ -54,6 +75,11 @@ function LangCard({ lang, config, status, busy, envStates, onInstallEnv, onToggl
|
|
|
54
75
|
status?.version ? ' · v' + status.version : '',
|
|
55
76
|
(phase === 'ready' && status?.root) ? ' · ' + String(status.root).split(/[\\/]/).pop() : ''),
|
|
56
77
|
status?.reason ? React.createElement('div', { className: 'vsm-mcp-error' }, status.reason) : null,
|
|
78
|
+
Array.isArray(status?.candidates) && status.candidates.length > 1
|
|
79
|
+
? React.createElement('div', { className: 'vsm-lsp-hint' },
|
|
80
|
+
'候选(' + status.candidates.length + '):' + status.candidates.map((c) =>
|
|
81
|
+
c.name + (c.version ? ' v' + c.version : '') + (c.chosen ? '(当前)' : '')).join(' · '))
|
|
82
|
+
: null,
|
|
57
83
|
Array.isArray(status?.missingEnv) && status.missingEnv.length
|
|
58
84
|
? React.createElement('div', { className: 'vsm-lsp-hint' },
|
|
59
85
|
'缺少运行环境(一键安装后自动生效):',
|
|
@@ -125,10 +151,10 @@ export function LspSettings() {
|
|
|
125
151
|
const [envLang, setEnvLang] = React.useState('')
|
|
126
152
|
|
|
127
153
|
const refreshServers = React.useCallback(() => {
|
|
128
|
-
Promise.all([rpc('edrv.lsp.configGet', {}), rpc('edrv.lsp.
|
|
129
|
-
.then(([cfg,
|
|
154
|
+
Promise.all([rpc('edrv.lsp.configGet', {}), rpc('edrv.lsp.detect', {})])
|
|
155
|
+
.then(([cfg, det]) => {
|
|
130
156
|
if (cfg?.ok) setConfig(cfg.config ?? {})
|
|
131
|
-
if (
|
|
157
|
+
if (det?.ok) setServers(det.servers ?? [])
|
|
132
158
|
setError('')
|
|
133
159
|
})
|
|
134
160
|
.catch((e) => setError(String(e)))
|
|
@@ -282,6 +308,7 @@ export function LspSettings() {
|
|
|
282
308
|
.then((res) => {
|
|
283
309
|
if (!res?.ok) { setError(res?.error ?? '安装失败'); return }
|
|
284
310
|
void refreshExt()
|
|
311
|
+
void refreshServers() // 新装 LSP 使对应语言卡片出现
|
|
285
312
|
})
|
|
286
313
|
.catch((e) => setError(String(e)))
|
|
287
314
|
.finally(() => setBusy(''))
|
|
@@ -296,6 +323,7 @@ export function LspSettings() {
|
|
|
296
323
|
if (!res?.ok) { setError(res?.error ?? '安装失败'); return }
|
|
297
324
|
setVsixPath('')
|
|
298
325
|
void refreshExt()
|
|
326
|
+
void refreshServers() // 新装 LSP 使对应语言卡片出现
|
|
299
327
|
})
|
|
300
328
|
.catch((e) => setError(String(e)))
|
|
301
329
|
.finally(() => setBusy(''))
|
|
@@ -309,6 +337,7 @@ export function LspSettings() {
|
|
|
309
337
|
.then((res) => {
|
|
310
338
|
if (!res?.ok) setError(res?.error ?? '卸载失败')
|
|
311
339
|
void refreshExt()
|
|
340
|
+
void refreshServers() // 卸载后对应语言卡片按最新检测隐匿
|
|
312
341
|
})
|
|
313
342
|
.catch((e) => setError(String(e)))
|
|
314
343
|
.finally(() => setBusy(''))
|
|
@@ -321,6 +350,7 @@ export function LspSettings() {
|
|
|
321
350
|
.then((res) => {
|
|
322
351
|
if (!res?.ok) setError(res?.error ?? '更新失败')
|
|
323
352
|
void refreshExt()
|
|
353
|
+
void refreshServers() // 更新后选用版本随之变化
|
|
324
354
|
})
|
|
325
355
|
.catch((e) => setError(String(e)))
|
|
326
356
|
.finally(() => setBusy(''))
|
|
@@ -344,14 +374,24 @@ export function LspSettings() {
|
|
|
344
374
|
|
|
345
375
|
let body = null
|
|
346
376
|
if (tab === 'servers') {
|
|
377
|
+
// 动态显示:检测结果(detect 全量)驱动卡片可见性;未检出语言灰字列出
|
|
378
|
+
const detected = Array.isArray(servers) ? servers : []
|
|
379
|
+
const visible = detected.filter((s) => langVisible(s, config))
|
|
380
|
+
const hidden = detected.filter((s) => !visible.includes(s))
|
|
347
381
|
body = panel('语言服务器', React.createElement(React.Fragment, null,
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
382
|
+
visible.length ? visible.map((status) => {
|
|
383
|
+
const meta = langMeta(status.languageId)
|
|
384
|
+
return React.createElement(LangCard, {
|
|
385
|
+
key: status.languageId, lang: { id: status.languageId, label: meta.label, hint: meta.hint },
|
|
386
|
+
config: config[status.languageId] ?? {},
|
|
387
|
+
status,
|
|
388
|
+
busy, envStates, onInstallEnv: installEnv, onToggle: toggleLang, onSave: saveLang, onRedetect: redetectLang,
|
|
389
|
+
})
|
|
390
|
+
}) : React.createElement('div', { className: 'vsm-mcp-empty' },
|
|
391
|
+
'未检测到已安装的语言服务器;可切换到「市场」搜索安装(如 sumneko.lua),或安装本地 .vsix 后刷新。'),
|
|
392
|
+
hidden.length ? React.createElement('div', { className: 'vsm-lsp-hint' },
|
|
393
|
+
'未检测到:' + hidden.map((s) => langMeta(s.languageId).label).join('、') + ' —— 可在「市场」安装后刷新。') : null,
|
|
394
|
+
React.createElement('div', { className: 'vsm-lsp-hint' }, '提示:打开对应语言的文件即自动(惰性)启动语言服务器;未配置时编辑器功能不受影响,大纲回退内置解析。')))
|
|
355
395
|
} else if (tab === 'installed') {
|
|
356
396
|
body = panel('已安装扩展', React.createElement(React.Fragment, null,
|
|
357
397
|
React.createElement('div', { className: 'vsm-lsp-hint' }, '已安装的语言服务器扩展(存于 ~/.dsh/dsh-vscode-mode/extensions/)。语言服务器被自动发现为「扩展」源。'),
|
package/src/index.ts
CHANGED
|
@@ -20,7 +20,6 @@ import { sweepTreeCache } from './paths.js'
|
|
|
20
20
|
import { PLUGIN_NAME, buildReport } from './compat.js'
|
|
21
21
|
import { createLspManager } from './lsp/manager.js'
|
|
22
22
|
import { createLspRpc } from './lsp/rpc.js'
|
|
23
|
-
import { installLspSettingsSection } from './lsp/settings.js'
|
|
24
23
|
import { disposeAllServers } from './lsp/transport.js'
|
|
25
24
|
import { installRulesSection } from './rules.js'
|
|
26
25
|
import type { RpcHandlerMap } from './shared/rpc.js'
|
|
@@ -45,7 +44,7 @@ export function apply(ctx: Ctx, config?: unknown): void {
|
|
|
45
44
|
/** 兼容性警告收集(route 护栏等写入,启动日志一并输出)。 */
|
|
46
45
|
const warnings: string[] = []
|
|
47
46
|
setupOpenSettings(ctx, config, () => {})
|
|
48
|
-
|
|
47
|
+
/** LSP 配置为配置值模式:插件组合配置 + 会话内运行时覆盖(原 settings section 命名空间不合法,见 lsp/config.ts)。 */
|
|
49
48
|
/** 规则注入 section(~/.dsh/rules 与 <工作区>/.dsh/rules;旧版 DSH 无 systemPrompt 时静默降级)。 */
|
|
50
49
|
const rulesInstalled = installRulesSection(ctx)
|
|
51
50
|
if (!rulesInstalled) ctx.logger?.warn?.('[' + name + '] 未检测到 systemPrompt 服务,规则仅可管理不注入')
|
package/src/lsp/config.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode host — LSP 配置读取与 provider 解析装配。
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 配置来源优先级:运行时覆盖(设置页保存,重启失效)> 插件组合配置 > 默认。
|
|
4
|
+
* 原 settings section(ns dsh-vscode-mode.languageServers)命名空间不合法
|
|
5
|
+
* (DSH settings 仅允许 /^[a-z][a-z0-9-]*$/,点号命名空间永久不可注册),
|
|
6
|
+
* 已降级为配置值(插件组合配置 + 会话内运行时覆盖),不再尝试 settings 安装。
|
|
7
|
+
* 作者 ddj 2026-08-27 / 2026-09-02
|
|
5
8
|
*/
|
|
6
|
-
import type { Ctx } from '../store.js'
|
|
7
9
|
import { PROVIDER_RESOLVERS, type LspProviderSpec } from './providers.js'
|
|
8
10
|
import { provisionRuntime } from './dotnetProvision.js'
|
|
9
11
|
|
|
@@ -21,10 +23,10 @@ export interface LspConfig {
|
|
|
21
23
|
[languageId: string]: LspLangConfig | undefined
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
export const LSP_SETTINGS_NS = 'dsh-vscode-mode.languageServers'
|
|
25
26
|
export const LSP_LANGUAGES = ['lua', 'csharp'] as const
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
/** 清洗单语言配置:仅保留合法字段并剥离 undefined(稀疏覆盖语义,清空字段即回落组合配置)。 */
|
|
29
|
+
export function sanitizeLang(raw: unknown): LspLangConfig | undefined {
|
|
28
30
|
if (!raw || typeof raw !== 'object') return undefined
|
|
29
31
|
const obj = raw as Record<string, unknown>
|
|
30
32
|
const out: LspLangConfig = {}
|
|
@@ -46,25 +48,7 @@ export function configFromPlugin(config: unknown): LspConfig {
|
|
|
46
48
|
return out
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
/**
|
|
50
|
-
export function configFromSettings(ctx: Ctx): LspConfig {
|
|
51
|
-
try {
|
|
52
|
-
const settings = ctx.get('settings')
|
|
53
|
-
const descriptor = settings?.describe?.({ redactSecrets: true })?.find((item: { ns?: string }) => item.ns === LSP_SETTINGS_NS)
|
|
54
|
-
const value = descriptor?.value
|
|
55
|
-
if (!value || typeof value !== 'object') return {}
|
|
56
|
-
const out: LspConfig = {}
|
|
57
|
-
for (const lang of LSP_LANGUAGES) {
|
|
58
|
-
const v = sanitizeLang((value as Record<string, unknown>)[lang])
|
|
59
|
-
if (v) out[lang] = v
|
|
60
|
-
}
|
|
61
|
-
return out
|
|
62
|
-
} catch (error) {
|
|
63
|
-
return {}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** 合并配置:settings 覆盖插件配置。 */
|
|
51
|
+
/** 合并配置:上层(运行时覆盖/settings)覆盖插件组合配置。 */
|
|
68
52
|
export function mergeConfig(a: LspConfig, b: LspConfig): LspConfig {
|
|
69
53
|
const out: LspConfig = { ...a }
|
|
70
54
|
for (const lang of LSP_LANGUAGES) {
|
|
@@ -77,17 +61,17 @@ export function mergeConfig(a: LspConfig, b: LspConfig): LspConfig {
|
|
|
77
61
|
}
|
|
78
62
|
|
|
79
63
|
/**
|
|
80
|
-
* 解析某语言的 provider spec
|
|
81
|
-
* @author ddj 2026年08月27号
|
|
82
|
-
* @param ctx DSH 上下文
|
|
64
|
+
* 解析某语言的 provider spec(插件组合配置 + 运行时覆盖层合并后)。
|
|
65
|
+
* @author ddj 2026年08月27号 / 2026年09月02号
|
|
83
66
|
* @param pluginConfig 插件组合配置
|
|
84
67
|
* @param languageId 语言 id
|
|
68
|
+
* @param override 运行时覆盖层(设置页保存值,重启失效;缺省为空)
|
|
85
69
|
* @returns provider 规格
|
|
86
70
|
*/
|
|
87
|
-
export function resolveProviderSpec(
|
|
71
|
+
export function resolveProviderSpec(pluginConfig: unknown, languageId: string, override?: LspConfig): LspProviderSpec {
|
|
88
72
|
const resolver = PROVIDER_RESOLVERS[languageId]
|
|
89
73
|
if (!resolver) return { languageId, kind: 'none', argv: [], ready: false, reason: '不支持的语言:' + languageId }
|
|
90
|
-
const merged = mergeConfig(configFromPlugin(pluginConfig),
|
|
74
|
+
const merged = mergeConfig(configFromPlugin(pluginConfig), override ?? {})
|
|
91
75
|
const lang = merged[languageId]
|
|
92
76
|
if (lang && lang.enabled === false) {
|
|
93
77
|
return { languageId, kind: 'none', argv: [], ready: false, reason: '已在设置中禁用' }
|
package/src/lsp/providers.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { dirname, join, sep } from 'node:path'
|
|
|
12
12
|
import { isAbsolute } from 'node:path'
|
|
13
13
|
import { extensionsRoot } from './extmgr.js'
|
|
14
14
|
import { dshHome, lspSpecCacheFile } from '../paths.js'
|
|
15
|
+
import type { LspCandidate } from '../shared/lsp.js'
|
|
15
16
|
|
|
16
17
|
/** provider 来源标记(与 shared/lsp.ts 的 status.source 一致)。 */
|
|
17
18
|
export type LspProviderKind = 'extension' | 'discover' | 'manual' | 'none'
|
|
@@ -237,26 +238,33 @@ export function candidateEmmyLua(home = dshHome()): { path: string; version?: st
|
|
|
237
238
|
return candidates[0] ?? null
|
|
238
239
|
}
|
|
239
240
|
|
|
240
|
-
/**
|
|
241
|
-
|
|
241
|
+
/**
|
|
242
|
+
* LuaLS 自动发现候选(VSCode 扩展目录 + extmgr 目录下 sumneko.lua-* 的 server/bin 可执行)。
|
|
243
|
+
* 多版本并存时按清单版本降序、路径升序排序(与 EmmyLua/DotRush 发现的排序策略一致)。
|
|
244
|
+
* @author ddj 2026年09月03号
|
|
245
|
+
* @param home DSH home(缺省真实;测试可注入临时目录)
|
|
246
|
+
* @returns 排序后的候选列表(首个为建议选用者)
|
|
247
|
+
*/
|
|
248
|
+
export function candidateLuaServers(home = dshHome()): { path: string; version?: string }[] {
|
|
242
249
|
const plat = platformServerDir()
|
|
243
250
|
const suffix = exeSuffix()
|
|
244
|
-
const out: string[] = []
|
|
251
|
+
const out: { path: string; version?: string }[] = []
|
|
245
252
|
for (const dir of vscodeExtensionsDirs(home)) {
|
|
246
253
|
for (const extDir of listMatchingDirs(dir, 'sumneko.lua-')) {
|
|
247
254
|
const binDir = join(extDir, 'server', 'bin')
|
|
248
255
|
const binName = 'lua-language-server' + suffix
|
|
249
256
|
// 新布局(3.x 平台变体 vsix):server/bin/xxx.exe;旧布局:server/bin/<平台>/xxx.exe
|
|
250
257
|
const bin = existsSync(join(binDir, binName)) ? join(binDir, binName) : join(binDir, plat, binName)
|
|
251
|
-
if (existsSync(bin) && !out.
|
|
258
|
+
if (existsSync(bin) && !out.some((c) => c.path === bin)) out.push({ path: bin, version: manifestVersionOf(extDir) })
|
|
252
259
|
}
|
|
253
260
|
}
|
|
254
|
-
return out
|
|
261
|
+
return out.sort((a, b) => (b.version ?? '').localeCompare(a.version ?? '', undefined, { numeric: true }) || a.path.localeCompare(b.path))
|
|
255
262
|
}
|
|
256
263
|
|
|
257
|
-
/** C# 服务器发现结果(ms-Roslyn / DotRush / OmniSharp + 系统 dotnet
|
|
264
|
+
/** C# 服务器发现结果(ms-Roslyn / DotRush / OmniSharp + 系统 dotnet;各取清单版本最高者)。 */
|
|
258
265
|
export interface CSharpCandidates {
|
|
259
266
|
roslynDll?: string
|
|
267
|
+
roslynVersion?: string
|
|
260
268
|
dotrushDll?: string
|
|
261
269
|
dotrushVersion?: string
|
|
262
270
|
omnisharpExe?: string
|
|
@@ -266,31 +274,44 @@ export interface CSharpCandidates {
|
|
|
266
274
|
/**
|
|
267
275
|
* C# 自动发现候选:ms-dotnettools.csharp 的 Roslyn dll(需 dotnet)/ OmniSharp 可执行,
|
|
268
276
|
* 以及 DotRush 扩展自带服务器(extension/bin/LanguageServer/DotRush.dll,需对应 .NET 运行时)。
|
|
277
|
+
* 同类多版本并存时取清单版本最高者(与 EmmyLua/DotRush 发现的排序策略一致)。
|
|
269
278
|
* @author ddj 2026年09月03号
|
|
270
279
|
* @param home DSH home(缺省真实;测试可注入临时目录)
|
|
271
280
|
* @returns 发现结果
|
|
272
281
|
*/
|
|
273
282
|
export function candidateCSharpServers(home = dshHome()): CSharpCandidates {
|
|
274
283
|
const out: CSharpCandidates = {}
|
|
275
|
-
|
|
284
|
+
/** 命中收集 → 清单版本降序取最高(平级按路径稳定排序)。 */
|
|
285
|
+
const bestOf = (hits: { entry: string; version?: string }[]): { entry: string; version?: string } | null =>
|
|
286
|
+
hits.sort((a, b) => (b.version ?? '').localeCompare(a.version ?? '', undefined, { numeric: true }) || a.entry.localeCompare(b.entry))[0] ?? null
|
|
287
|
+
const roslynHits: { entry: string; version?: string }[] = []
|
|
288
|
+
const omniHits: { entry: string; version?: string }[] = []
|
|
289
|
+
const dotrushHits: { entry: string; version?: string }[] = []
|
|
276
290
|
for (const dir of vscodeExtensionsDirs(home)) {
|
|
277
291
|
for (const extDir of listMatchingDirs(dir, 'ms-dotnettools.csharp-')) {
|
|
292
|
+
const version = manifestVersionOf(extDir)
|
|
278
293
|
const roslyn = join(extDir, '.roslyn', 'Microsoft.CodeAnalysis.LanguageServer.dll')
|
|
279
|
-
if (
|
|
294
|
+
if (existsSync(roslyn)) roslynHits.push({ entry: roslyn, version })
|
|
280
295
|
const omni = join(extDir, '.omnisharp', 'OmniSharp' + exeSuffix())
|
|
281
|
-
if (
|
|
296
|
+
if (existsSync(omni)) omniHits.push({ entry: omni, version })
|
|
282
297
|
}
|
|
283
298
|
for (const extDir of listMatchingDirs(dir, 'nromanov.dotrush-')) {
|
|
284
299
|
const dll = join(extDir, 'extension', 'bin', 'LanguageServer', 'DotRush.dll')
|
|
285
300
|
if (!existsSync(dll)) continue
|
|
286
|
-
dotrushHits.push({ dll, version: manifestVersionOf(extDir) })
|
|
301
|
+
dotrushHits.push({ entry: dll, version: manifestVersionOf(extDir) })
|
|
287
302
|
}
|
|
288
303
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
out.
|
|
293
|
-
|
|
304
|
+
const roslynBest = bestOf(roslynHits)
|
|
305
|
+
if (roslynBest) {
|
|
306
|
+
out.roslynDll = roslynBest.entry
|
|
307
|
+
out.roslynVersion = roslynBest.version
|
|
308
|
+
}
|
|
309
|
+
const omniBest = bestOf(omniHits)
|
|
310
|
+
if (omniBest) out.omnisharpExe = omniBest.entry
|
|
311
|
+
const dotrushBest = bestOf(dotrushHits)
|
|
312
|
+
if (dotrushBest) {
|
|
313
|
+
out.dotrushDll = dotrushBest.entry
|
|
314
|
+
out.dotrushVersion = dotrushBest.version
|
|
294
315
|
}
|
|
295
316
|
const dotnet = findDotnet()
|
|
296
317
|
if (dotnet) out.dotnet = dotnet
|
|
@@ -367,7 +388,8 @@ export function resolveLuaProvider(manual?: { command?: string; path?: string },
|
|
|
367
388
|
const emmy = candidateEmmyLua(h)
|
|
368
389
|
if (emmy) return { ...base, kind: 'extension', argv: [emmy.path], ready: true, version: emmy.version, providerName: 'EmmyLua' }
|
|
369
390
|
const candidates = candidateLuaServers(h)
|
|
370
|
-
|
|
391
|
+
const best = candidates[0]
|
|
392
|
+
if (best) return { ...base, kind: 'discover', argv: [best.path], cwd: undefined, ready: true, version: best.version, providerName: 'LuaLS' }
|
|
371
393
|
const inPath = findInPath('lua-language-server' + exeSuffix())
|
|
372
394
|
if (inPath) return { ...base, kind: 'discover', argv: [inPath], ready: true }
|
|
373
395
|
return { ...base, kind: 'none', argv: [], ready: false, reason: manualResolved.reason ?? '未发现 lua-language-server(可安装 LuaLS 或手动指定路径)' }
|
|
@@ -429,6 +451,44 @@ export const PROVIDER_RESOLVERS: Record<string, ProviderResolver> = {
|
|
|
429
451
|
csharp: resolveCSharpProvider,
|
|
430
452
|
}
|
|
431
453
|
|
|
454
|
+
/**
|
|
455
|
+
* 汇总某语言当前检测到的候选服务器(每类服务器一条、取该类最高版本;设置页展示用)。
|
|
456
|
+
* chosen 以候选路径命中 spec.argv 判定;未选用(kind=none/manual 不匹配)时不标记。
|
|
457
|
+
* @author ddj 2026年09月03号
|
|
458
|
+
* @param languageId 语言 id
|
|
459
|
+
* @param spec 已解析的 provider 规格
|
|
460
|
+
* @param home DSH home(缺省真实;测试可注入临时目录)
|
|
461
|
+
* @returns 候选列表(不支持的语言/检测异常返回空)
|
|
462
|
+
*/
|
|
463
|
+
export function candidatesFor(languageId: string, spec: LspProviderSpec, home = dshHome()): LspCandidate[] {
|
|
464
|
+
/** 候选路径命中 spec.argv 时补 chosen 标记。 */
|
|
465
|
+
const markChosen = (candidate: LspCandidate): LspCandidate =>
|
|
466
|
+
spec.ready && candidate.path && spec.argv.includes(candidate.path) ? { ...candidate, chosen: true } : candidate
|
|
467
|
+
try {
|
|
468
|
+
if (languageId === 'lua') {
|
|
469
|
+
const out: LspCandidate[] = []
|
|
470
|
+
const emmy = candidateEmmyLua(home)
|
|
471
|
+
if (emmy) out.push(markChosen({ name: 'EmmyLua', version: emmy.version, path: emmy.path }))
|
|
472
|
+
const lua = candidateLuaServers(home)[0]
|
|
473
|
+
if (lua) out.push(markChosen({ name: 'LuaLS(sumneko.lua)', version: lua.version, path: lua.path }))
|
|
474
|
+
const inPath = findInPath('lua-language-server' + exeSuffix())
|
|
475
|
+
if (inPath) out.push(markChosen({ name: 'lua-language-server(PATH)', path: inPath }))
|
|
476
|
+
return out
|
|
477
|
+
}
|
|
478
|
+
if (languageId === 'csharp') {
|
|
479
|
+
const out: LspCandidate[] = []
|
|
480
|
+
const found = candidateCSharpServers(home)
|
|
481
|
+
if (found.roslynDll) out.push(markChosen({ name: 'Roslyn(ms-dotnettools.csharp)', version: found.roslynVersion, path: found.roslynDll }))
|
|
482
|
+
if (found.dotrushDll) out.push(markChosen({ name: 'DotRush', version: found.dotrushVersion, path: found.dotrushDll }))
|
|
483
|
+
if (found.omnisharpExe) out.push(markChosen({ name: 'OmniSharp', path: found.omnisharpExe }))
|
|
484
|
+
return out
|
|
485
|
+
}
|
|
486
|
+
return []
|
|
487
|
+
} catch (error) {
|
|
488
|
+
return []
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
432
492
|
/** 手动配置是否为绝对路径可执行(供设置页判断输入类型)。 */
|
|
433
493
|
export function looksAbsolute(value: string): boolean {
|
|
434
494
|
return isAbsolute(value)
|
package/src/lsp/rpc.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode host — edrv.lsp.* RPC handlers。
|
|
3
3
|
* 文档跟踪(openDocs 集合 + root|lang 引用计数)→ 懒启动服务器、用完释放。
|
|
4
|
-
*
|
|
4
|
+
* LSP 配置降级为配置值:插件组合配置 + 会话内运行时覆盖层(原 settings section
|
|
5
|
+
* 命名空间不合法,永久不可注册,见 lsp/config.ts 头注释)。
|
|
6
|
+
* 作者 ddj 2026-08-27 / 2026-09-02
|
|
5
7
|
*/
|
|
6
8
|
import type { Ctx } from '../store.js'
|
|
7
9
|
import { searchRoot } from '../search/ripgrep.js'
|
|
8
10
|
import { sessionOf, cwdOf } from '../registry.js'
|
|
9
|
-
import { resolveProviderSpec, langOfPath, configFromPlugin,
|
|
10
|
-
import { clearProviderCache } from './providers.js'
|
|
11
|
+
import { resolveProviderSpec, langOfPath, configFromPlugin, mergeConfig, sanitizeLang, LSP_LANGUAGES, type LspConfig, type LspLangConfig } from './config.js'
|
|
12
|
+
import { clearProviderCache, candidatesFor } from './providers.js'
|
|
11
13
|
import { onRuntimeProvisioned, envInstallStates } from './dotnetProvision.js'
|
|
12
14
|
import { envRequirementsFor, installRequirement } from './envRequirements.js'
|
|
13
15
|
import type { LspManager } from './manager.js'
|
|
@@ -116,6 +118,8 @@ function createDocTracker() {
|
|
|
116
118
|
export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMap>; disposeSession: (sessionId: string) => void } {
|
|
117
119
|
const { ctx, pluginConfig, manager } = deps
|
|
118
120
|
const tracker = createDocTracker()
|
|
121
|
+
/** 运行时覆盖层:设置页保存值(内存态,重启回落组合配置值;每 apply 新建,插件重载自动复位)。 */
|
|
122
|
+
const runtimeConfig: LspConfig = {}
|
|
119
123
|
|
|
120
124
|
// 运行时自动配置成功:清 provider 缓存并重置 C# 服务器,下一次文档同步即用新运行时重启
|
|
121
125
|
onRuntimeProvisioned(() => {
|
|
@@ -139,7 +143,7 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
|
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
const acquireServer = (root: string, lang: string) =>
|
|
142
|
-
manager.acquire(root, lang, (languageId: string) => resolveProviderSpec(
|
|
146
|
+
manager.acquire(root, lang, (languageId: string) => resolveProviderSpec(pluginConfig, languageId, runtimeConfig))
|
|
143
147
|
|
|
144
148
|
const serverOf = (root: string, lang: string) =>
|
|
145
149
|
manager.peek(root, lang)
|
|
@@ -147,10 +151,11 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
|
|
|
147
151
|
const rootLocations = (locations: LspLocation[], root: string): LspLocation[] =>
|
|
148
152
|
locations.map((location) => ({ ...location, root }))
|
|
149
153
|
|
|
150
|
-
/** 当前 provider 检测结论 → 设置页 idle 状态(不启动 server
|
|
154
|
+
/** 当前 provider 检测结论 → 设置页 idle 状态(不启动 server);附带未满足的环境需求与候选服务器。 */
|
|
151
155
|
const detectedStatus = (languageId: string, root?: string): LspServerStatus => {
|
|
152
|
-
const spec = resolveProviderSpec(
|
|
156
|
+
const spec = resolveProviderSpec(pluginConfig, languageId, runtimeConfig)
|
|
153
157
|
const missingEnv = envRequirementsFor(languageId, dshHome())
|
|
158
|
+
const candidates = candidatesFor(languageId, spec)
|
|
154
159
|
return {
|
|
155
160
|
languageId,
|
|
156
161
|
source: spec.kind,
|
|
@@ -160,6 +165,7 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
|
|
|
160
165
|
providerName: spec.providerName,
|
|
161
166
|
root,
|
|
162
167
|
missingEnv: missingEnv.length ? missingEnv : undefined,
|
|
168
|
+
candidates: candidates.length ? candidates : undefined,
|
|
163
169
|
}
|
|
164
170
|
}
|
|
165
171
|
|
|
@@ -199,34 +205,32 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
|
|
|
199
205
|
return { ok: true, servers: manager.statusAll().filter((status) => status.root === sc.root) }
|
|
200
206
|
},
|
|
201
207
|
|
|
208
|
+
'edrv.lsp.detect': async () => {
|
|
209
|
+
// 全支持语言的检测结果(不启动服务器):设置页动态渲染卡片 + 候选展示
|
|
210
|
+
return { ok: true, servers: LSP_LANGUAGES.map((lang) => detectedStatus(lang)) }
|
|
211
|
+
},
|
|
212
|
+
|
|
202
213
|
'edrv.lsp.configGet': async () => {
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
const merged: LspConfig = { ...plugin }
|
|
206
|
-
for (const lang of Object.keys(settings)) merged[lang] = { ...plugin[lang], ...settings[lang] }
|
|
214
|
+
// 配置值降级:插件组合配置 + 运行时覆盖层(原 settings section 已废弃)
|
|
215
|
+
const merged = mergeConfig(configFromPlugin(pluginConfig), runtimeConfig)
|
|
207
216
|
return { ok: true, config: merged as unknown as Record<string, unknown> }
|
|
208
217
|
},
|
|
209
218
|
|
|
210
219
|
'edrv.lsp.configUpdate': async (args) => {
|
|
211
220
|
const lang = args.languageId
|
|
212
|
-
if (!lang || !
|
|
221
|
+
if (!lang || !(LSP_LANGUAGES as readonly string[]).includes(lang)) {
|
|
213
222
|
return { ok: false, error: '不支持的语言:' + lang }
|
|
214
223
|
}
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
const target = next[lang]!
|
|
224
|
+
// 覆盖层基于当前运行时值增量修改;清空字段剥离 undefined → 回落组合配置
|
|
225
|
+
const target: LspLangConfig = { ...(runtimeConfig[lang] ?? {}) }
|
|
218
226
|
if (args.enabled !== undefined) target.enabled = args.enabled
|
|
219
227
|
if (args.command !== undefined) target.command = args.command || undefined
|
|
220
228
|
if (args.path !== undefined) target.path = args.path || undefined
|
|
229
|
+
runtimeConfig[lang] = sanitizeLang(target) ?? {}
|
|
221
230
|
try {
|
|
222
|
-
const settings = ctx.get('settings')
|
|
223
|
-
if (!settings?.update) return { ok: false, error: '设置服务不可用' }
|
|
224
|
-
await settings.update(LSP_SETTINGS_NS, { [lang]: target })
|
|
225
231
|
// 保存/切换后立即重新检测:清缓存 + 重置旧 server,旧状态不残留
|
|
226
232
|
const servers = await redetectLanguage(lang)
|
|
227
|
-
const
|
|
228
|
-
const stored = descriptor?.value as Record<string, unknown> | undefined
|
|
229
|
-
const merged: LspConfig = { ...configFromPlugin(pluginConfig), ...(stored as LspConfig) }
|
|
233
|
+
const merged = mergeConfig(configFromPlugin(pluginConfig), runtimeConfig)
|
|
230
234
|
return { ok: true, config: merged as unknown as Record<string, unknown>, servers }
|
|
231
235
|
} catch (error) {
|
|
232
236
|
return { ok: false, error: '配置保存失败:' + String(error) }
|
package/src/shared/lsp.ts
CHANGED
|
@@ -164,6 +164,14 @@ export interface LspEnvInstallState {
|
|
|
164
164
|
message?: string
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/** 同语言检测到的候选服务器(candidates 元素;chosen 标记当前选用者)。 */
|
|
168
|
+
export interface LspCandidate {
|
|
169
|
+
name: string // 显示名(如 EmmyLua / LuaLS / Roslyn)
|
|
170
|
+
version?: string // 扩展清单版本
|
|
171
|
+
path?: string // 服务器入口路径(argv 比对用)
|
|
172
|
+
chosen?: boolean // 是否为当前解析选用的服务器
|
|
173
|
+
}
|
|
174
|
+
|
|
167
175
|
/** 单语言服务器状态(edrv.lsp.status 载荷元素)。 */
|
|
168
176
|
export interface LspServerStatus {
|
|
169
177
|
languageId: string
|
|
@@ -176,6 +184,7 @@ export interface LspServerStatus {
|
|
|
176
184
|
root?: string // 绑定工作区根(相对显示)
|
|
177
185
|
providerName?: string // provider 名称(如 EmmyLua / LuaLS)
|
|
178
186
|
missingEnv?: LspMissingEnv[] // 未满足的环境需求(无则省略)
|
|
187
|
+
candidates?: LspCandidate[] // 同语言检测到的候选服务器(设置页展示用,无则省略)
|
|
179
188
|
}
|
|
180
189
|
|
|
181
190
|
/**
|
package/src/shared/rpc.ts
CHANGED
|
@@ -137,6 +137,7 @@ export interface RpcRequestMap {
|
|
|
137
137
|
'vscode.devFormSet': { enabled: boolean; path?: string }
|
|
138
138
|
'compat': {}
|
|
139
139
|
'edrv.lsp.status': { sessionId?: string }
|
|
140
|
+
'edrv.lsp.detect': {}
|
|
140
141
|
'edrv.lsp.sync': { sessionId?: string; path: string; text: string; version: number }
|
|
141
142
|
'edrv.lsp.close': { sessionId?: string; path: string }
|
|
142
143
|
'edrv.lsp.definition': { sessionId?: string; path: string; position: LspPosition }
|
|
@@ -208,6 +209,7 @@ export interface RpcOkMap {
|
|
|
208
209
|
'vscode.devFormSet': { devForm: DevFormInfo; restart: boolean }
|
|
209
210
|
'compat': { report: CompatReport }
|
|
210
211
|
'edrv.lsp.status': { servers: LspServerStatus[] }
|
|
212
|
+
'edrv.lsp.detect': { servers: LspServerStatus[] }
|
|
211
213
|
'edrv.lsp.sync': object
|
|
212
214
|
'edrv.lsp.close': object
|
|
213
215
|
'edrv.lsp.definition': { locations: LspLocation[]; truncated?: boolean }
|
package/src/lsp/settings.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-vscode-mode host — LSP 配置设置 section 安装(版本自适应;缺失/失败不抛错)。
|
|
3
|
-
* ns:dsh-vscode-mode.languageServers;每语言 { enabled, command, path }。
|
|
4
|
-
* 安装策略分派见 fileOpenSettings.runSettingsInstall(rc 线 legacy / alpha 线服务方法)。
|
|
5
|
-
* 作者 ddj 2026-08-27 / 2026-09-02
|
|
6
|
-
*/
|
|
7
|
-
import type { Ctx } from '../store.js'
|
|
8
|
-
import { LSP_LANGUAGES, LSP_SETTINGS_NS, type LspConfig } from './config.js'
|
|
9
|
-
import { loadSettingsDeps, runSettingsInstall, type SettingsDeps } from '../fileOpenSettings.js'
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* 安装 LSP 设置 section。
|
|
13
|
-
* @author ddj 2026年08月27号
|
|
14
|
-
* @param ctx DSH 上下文
|
|
15
|
-
* @param entry 初始配置
|
|
16
|
-
* @returns 是否成功安装
|
|
17
|
-
*/
|
|
18
|
-
export async function installLspSettingsSection(ctx: Ctx, entry: LspConfig): Promise<boolean> {
|
|
19
|
-
let deps: SettingsDeps | null = null
|
|
20
|
-
try {
|
|
21
|
-
deps = await loadSettingsDeps()
|
|
22
|
-
} catch {
|
|
23
|
-
/* 装载失败按缺失处理 */
|
|
24
|
-
}
|
|
25
|
-
if (!deps) return false
|
|
26
|
-
try {
|
|
27
|
-
// schemastery 无 .optional(),对象字段默认即可选;.default 必给避免 undefined 覆盖
|
|
28
|
-
const z = deps.z
|
|
29
|
-
const langShape: Record<string, unknown> = {}
|
|
30
|
-
for (const lang of LSP_LANGUAGES) {
|
|
31
|
-
langShape[lang] = z.object({
|
|
32
|
-
enabled: z.boolean(),
|
|
33
|
-
command: z.string(),
|
|
34
|
-
path: z.string(),
|
|
35
|
-
}).default({})
|
|
36
|
-
}
|
|
37
|
-
const schema = z.object(langShape).default({})
|
|
38
|
-
const strategy = await runSettingsInstall(ctx, LSP_SETTINGS_NS, schema, entry, {
|
|
39
|
-
setSource: () => {},
|
|
40
|
-
onChange: () => {},
|
|
41
|
-
})
|
|
42
|
-
return strategy === 'legacy' || strategy === 'service'
|
|
43
|
-
} catch (error) {
|
|
44
|
-
// 设置 section 安装失败不致命:降级为无设置 UI,LSP 核心功能不受影响
|
|
45
|
-
console.error('[dsh-vscode-mode] LSP 设置 section 安装失败:' + String(error))
|
|
46
|
-
return false
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export type { SettingsDeps }
|