dsh-vscode-mode 0.1.32 → 0.1.35

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.
@@ -0,0 +1,133 @@
1
+ /**
2
+ * dsh-vscode-mode host — LSP 服务器管理器。
3
+ * 实例注册表(key=root|languageId,refcount 共享),崩溃退避重启,session/disposed 清理。
4
+ * 作者 ddj 2026-08-27
5
+ */
6
+ import { createLspServer, type LspServer } from './server.js'
7
+ import type { LspProviderSpec } from './providers.js'
8
+ import type { LspServerStatus } from '../shared/lsp.js'
9
+
10
+ interface Entry {
11
+ key: string
12
+ server: LspServer
13
+ refs: number
14
+ failures: number
15
+ restartTimer: ReturnType<typeof setTimeout> | null
16
+ }
17
+
18
+ const BACKOFF_MS = [1000, 5000, 30000]
19
+ const MAX_FAILURES = 3
20
+
21
+ export interface LspManager {
22
+ /** 获取(或惰性创建)某工作区+语言的服务器;refs+1。 */
23
+ acquire(root: string, languageId: string, resolveSpec: (languageId: string) => LspProviderSpec): LspServer
24
+ /** 释放引用;refs 归零时停止服务器。 */
25
+ release(root: string, languageId: string): void
26
+ /** 释放工作区下全部语言(session/disposed 用)。 */
27
+ releaseRoot(root: string): void
28
+ /** 状态快照。 */
29
+ statusAll(): LspServerStatus[]
30
+ /** 查询已注册的工作区+语言服务器,不增加引用计数。 */
31
+ peek(root: string, languageId: string): LspServer | undefined
32
+ /** 停止并清空全部(卸载/退出)。 */
33
+ disposeAll(): Promise<void>
34
+ /** 监听状态变化(用于 UI)。 */
35
+ onStatusChange: ((status: LspServerStatus) => void) | null
36
+ }
37
+
38
+ /**
39
+ * 创建 LSP 管理器。
40
+ * @author ddj 2026年08月27号
41
+ * @param logger 诊断日志
42
+ * @returns 管理器
43
+ */
44
+ export function createLspManager(logger?: (line: string) => void): LspManager {
45
+ const entries = new Map<string, Entry>()
46
+ const keyOf = (root: string, languageId: string): string => root + '|' + languageId
47
+
48
+ const log = (line: string): void => logger?.('[lsp-manager] ' + line)
49
+
50
+ /** 同步摘除注册表项并后台回收(refs 归零/工作区清理用;statusAll 立即可见归零)。 */
51
+ const dropEntry = (entry: Entry): void => {
52
+ if (entry.restartTimer) {
53
+ clearTimeout(entry.restartTimer)
54
+ entry.restartTimer = null
55
+ }
56
+ entries.delete(entry.key)
57
+ void entry.server.dispose().catch(() => {})
58
+ }
59
+
60
+ const scheduleRestart = (entry: Entry): void => {
61
+ if (entry.restartTimer) return
62
+ if (entry.failures >= MAX_FAILURES) {
63
+ log('unavailable after ' + entry.failures + ' failures: ' + entry.key)
64
+ return
65
+ }
66
+ const delay = BACKOFF_MS[Math.min(entry.failures, BACKOFF_MS.length - 1)]
67
+ entry.restartTimer = setTimeout(() => {
68
+ entry.restartTimer = null
69
+ if (!entries.has(entry.key)) return
70
+ void (async () => {
71
+ const ok = await entry.server.start()
72
+ if (!ok) entry.failures++
73
+ else entry.failures = 0
74
+ if (!ok && entries.has(entry.key)) scheduleRestart(entry)
75
+ })()
76
+ }, delay)
77
+ }
78
+
79
+ const manager: LspManager = {
80
+ acquire(root: string, languageId: string, resolveSpec: (languageId: string) => LspProviderSpec): LspServer {
81
+ const key = keyOf(root, languageId)
82
+ let entry = entries.get(key)
83
+ if (!entry) {
84
+ const spec = resolveSpec(languageId)
85
+ const server = createLspServer(spec, root, languageId, log)
86
+ server.onStateChange = (status) => manager.onStatusChange?.(status)
87
+ entry = { key, server, refs: 0, failures: 0, restartTimer: null }
88
+ entries.set(key, entry)
89
+ // 惰性启动:若 spec ready 才启动,否则保持 idle(供 status 展示 reason)
90
+ if (spec.ready) {
91
+ void server.start().then((ok) => {
92
+ if (!ok && entries.has(key)) entry!.failures++
93
+ })
94
+ }
95
+ }
96
+ entry.refs++
97
+ return entry.server
98
+ },
99
+
100
+ release(root: string, languageId: string): void {
101
+ const entry = entries.get(keyOf(root, languageId))
102
+ if (!entry) return
103
+ entry.refs = Math.max(0, entry.refs - 1)
104
+ if (entry.refs === 0) dropEntry(entry)
105
+ },
106
+
107
+ releaseRoot(root: string): void {
108
+ for (const entry of [...entries.values()]) {
109
+ if (entry.server.root === root) dropEntry(entry)
110
+ }
111
+ },
112
+
113
+ statusAll(): LspServerStatus[] {
114
+ return [...entries.values()].map((entry) => entry.server.status())
115
+ },
116
+
117
+ peek(root: string, languageId: string): LspServer | undefined {
118
+ return entries.get(keyOf(root, languageId))?.server
119
+ },
120
+
121
+ async disposeAll(): Promise<void> {
122
+ const list = [...entries.values()]
123
+ entries.clear()
124
+ for (const entry of list) {
125
+ if (entry.restartTimer) clearTimeout(entry.restartTimer)
126
+ await entry.server.dispose()
127
+ }
128
+ },
129
+
130
+ onStatusChange: null,
131
+ }
132
+ return manager
133
+ }
@@ -0,0 +1,235 @@
1
+ /**
2
+ * dsh-vscode-mode host — 语言服务器 provider 解析。
3
+ * 优先级:手动配置 > extmgr 扩展源(Phase 2 挂载) > 自动发现(~/.vscode/extensions、
4
+ * ~/.dsh/dsh-vscode-mode/extensions、PATH)。产物 LspProviderSpec:{ ready, argv, cwd, reason },
5
+ * 供 manager 启动服务器。
6
+ * 作者 ddj 2026-08-27
7
+ */
8
+ import { existsSync, readdirSync, type Dirent } from 'node:fs'
9
+ import { homedir } from 'node:os'
10
+ import { join, sep } from 'node:path'
11
+ import { isAbsolute } from 'node:path'
12
+ import { extensionsRoot } from './extmgr.js'
13
+ import { dshHome } from '../paths.js'
14
+
15
+ /** provider 来源标记(与 shared/lsp.ts 的 status.source 一致)。 */
16
+ export type LspProviderKind = 'extension' | 'discover' | 'manual' | 'none'
17
+
18
+ export interface LspProviderSpec {
19
+ languageId: string
20
+ kind: LspProviderKind
21
+ argv: string[]
22
+ cwd?: string
23
+ ready: boolean
24
+ reason?: string
25
+ }
26
+
27
+ /** 语言 → 扩展名(用于判断哪些文件触发该语言服务器)。 */
28
+ export const LSP_LANG_EXT: Record<string, string[]> = {
29
+ lua: ['lua'],
30
+ csharp: ['cs', 'csx'],
31
+ }
32
+
33
+ /** 平台目录名(LuaLS/OmniSharp 的 server/bin/<平台>)。 */
34
+ export function platformServerDir(platform = process.platform): string {
35
+ if (platform === 'win32') return 'Windows'
36
+ if (platform === 'darwin') return 'macOS'
37
+ return 'Linux'
38
+ }
39
+
40
+ /** exe 后缀(Windows 为 .exe)。 */
41
+ export function exeSuffix(platform = process.platform): string {
42
+ return platform === 'win32' ? '.exe' : ''
43
+ }
44
+
45
+ /** 列出目录下匹配前缀的子目录(如 ~/.vscode/extensions/sumneko.lua-*)。 */
46
+ export function listMatchingDirs(dir: string, prefix: string): string[] {
47
+ if (!existsSync(dir)) return []
48
+ let entries: Dirent[]
49
+ try {
50
+ entries = readdirSync(dir, { withFileTypes: true })
51
+ } catch (error) {
52
+ return []
53
+ }
54
+ return entries.filter((e) => e.isDirectory() && e.name.startsWith(prefix)).map((e) => join(dir, e.name))
55
+ }
56
+
57
+ /** VSCode 扩展目录(~/.vscode/extensions、~/.vscode-server/extensions + extmgr 目录;无则空数组)。 */
58
+ export function vscodeExtensionsDirs(home = dshHome()): string[] {
59
+ const userHome = homedir()
60
+ return [
61
+ join(userHome, '.vscode', 'extensions'),
62
+ join(userHome, '.vscode-server', 'extensions'),
63
+ extensionsRoot(home),
64
+ ].filter((dir) => existsSync(dir))
65
+ }
66
+
67
+ // --region 扩展源 provider 挂载点(extmgr 安装扩展后注册;kind=extension)
68
+ /** 扩展源 provider 注册表:languageId → 启动参数。 */
69
+ const extProviders = new Map<string, { argv: string[]; cwd?: string; version?: string }>()
70
+
71
+ /**
72
+ * 注册扩展源 provider(extmgr 安装的 VSIX 扩展调用;覆盖/清除传空 argv 可注销)。
73
+ * @author ddj 2026年08月27号
74
+ * @param languageId 语言 id
75
+ * @param argv 启动参数(argv[0] 为可执行)
76
+ * @param cwd 工作目录
77
+ * @param version 扩展版本(诊断展示)
78
+ */
79
+ export function registerExtensionProvider(languageId: string, argv: string[], cwd?: string, version?: string): void {
80
+ if (!argv.length) {
81
+ extProviders.delete(languageId)
82
+ return
83
+ }
84
+ extProviders.set(languageId, { argv, cwd, version })
85
+ }
86
+
87
+ /** 取扩展源 provider spec(未注册返回 null)。 */
88
+ export function extensionProviderFor(languageId: string): LspProviderSpec | null {
89
+ const p = extProviders.get(languageId)
90
+ if (!p || !p.argv.length) return null
91
+ return { languageId, kind: 'extension', argv: p.argv, cwd: p.cwd, ready: true }
92
+ }
93
+
94
+ /** 清空全部扩展源注册(插件卸载/扩展目录重建时)。 */
95
+ export function clearExtensionProviders(): void {
96
+ extProviders.clear()
97
+ }
98
+ // --endregion
99
+
100
+ /** PATH 中查找可执行文件(不含扩展名校验,交由 spawn 报错)。 */
101
+ export function findInPath(name: string): string | null {
102
+ const pathEnv = process.env.PATH ?? ''
103
+ const exts = process.platform === 'win32' ? ['', '.exe', '.cmd', '.bat'] : ['']
104
+ for (const dir of pathEnv.split(sep === '\\' ? ';' : ':')) {
105
+ if (!dir) continue
106
+ for (const ext of exts) {
107
+ const candidate = join(dir, name + ext)
108
+ if (existsSync(candidate)) return candidate
109
+ }
110
+ }
111
+ return null
112
+ }
113
+
114
+ /** 查找 dotnet(PATH + 常见安装路径)。 */
115
+ export function findDotnet(): string | null {
116
+ const inPath = findInPath('dotnet')
117
+ if (inPath) return inPath
118
+ const candidates = [
119
+ 'C:/Program Files/dotnet/dotnet.exe',
120
+ 'C:/Program Files (x86)/dotnet/dotnet.exe',
121
+ ]
122
+ for (const c of candidates) if (existsSync(c)) return c
123
+ return null
124
+ }
125
+
126
+ /** LuaLS 自动发现候选(VSCode 扩展目录 + extmgr 目录下 sumneko.lua-* 的 server/bin 可执行)。 */
127
+ export function candidateLuaServers(home = dshHome()): string[] {
128
+ const plat = platformServerDir()
129
+ const suffix = exeSuffix()
130
+ const out: string[] = []
131
+ for (const dir of vscodeExtensionsDirs(home)) {
132
+ for (const extDir of listMatchingDirs(dir, 'sumneko.lua-')) {
133
+ const binDir = join(extDir, 'server', 'bin')
134
+ const binName = 'lua-language-server' + suffix
135
+ // 新布局(3.x 平台变体 vsix):server/bin/xxx.exe;旧布局:server/bin/<平台>/xxx.exe
136
+ const bin = existsSync(join(binDir, binName)) ? join(binDir, binName) : join(binDir, plat, binName)
137
+ if (existsSync(bin) && !out.includes(bin)) out.push(bin)
138
+ }
139
+ }
140
+ return out
141
+ }
142
+
143
+ /** C# 自动发现候选:Roslyn dll(需 dotnet)或 OmniSharp 可执行。 */
144
+ export function candidateCSharpServers(home = dshHome()): { roslynDll?: string; omnisharpExe?: string; dotnet?: string } {
145
+ const out: { roslynDll?: string; omnisharpExe?: string; dotnet?: string } = {}
146
+ for (const dir of vscodeExtensionsDirs(home)) {
147
+ for (const extDir of listMatchingDirs(dir, 'ms-dotnettools.csharp-')) {
148
+ const roslyn = join(extDir, '.roslyn', 'Microsoft.CodeAnalysis.LanguageServer.dll')
149
+ if (!out.roslynDll && existsSync(roslyn)) out.roslynDll = roslyn
150
+ const omni = join(extDir, '.omnisharp', 'OmniSharp' + exeSuffix())
151
+ if (!out.omnisharpExe && existsSync(omni)) out.omnisharpExe = omni
152
+ }
153
+ }
154
+ const dotnet = findDotnet()
155
+ if (dotnet) out.dotnet = dotnet
156
+ return out
157
+ }
158
+
159
+ /**
160
+ * 手动配置归一化:path 指向可执行文件/目录,command 为可执行名(PATH 解析)。
161
+ * 未配置时返回空 reason(由各 resolve 兜底输出语言相关安装指引文案)。
162
+ * @author ddj 2026年08月27号
163
+ */
164
+ export function manualSpec(manual: { command?: string; path?: string } | undefined): { argv: string[]; cwd?: string; ready: boolean; reason?: string } {
165
+ if (!manual) return { argv: [], ready: false }
166
+ const pathValue = typeof manual.path === 'string' && manual.path.trim() ? manual.path.trim() : ''
167
+ const commandValue = typeof manual.command === 'string' && manual.command.trim() ? manual.command.trim() : ''
168
+ if (!pathValue && !commandValue) return { argv: [], ready: false }
169
+ if (pathValue) {
170
+ if (!existsSync(pathValue)) return { argv: [], ready: false, reason: '路径不存在:' + pathValue }
171
+ return { argv: [pathValue], ready: true }
172
+ }
173
+ const found = findInPath(commandValue)
174
+ if (!found) return { argv: [], ready: false, reason: 'PATH 中找不到命令:' + commandValue }
175
+ return { argv: [found], ready: true }
176
+ }
177
+
178
+ /**
179
+ * 解析 Lua provider(优先级:手动 > 扩展源 > 自动发现)。
180
+ * @author ddj 2026年08月27号
181
+ * @param manual 手动配置
182
+ * @param home DSH home(缺省真实;测试可注入临时目录)
183
+ * @returns provider 规格
184
+ */
185
+ export function resolveLuaProvider(manual?: { command?: string; path?: string }, home = dshHome()): LspProviderSpec {
186
+ const base = { languageId: 'lua' }
187
+ const manualResolved = manualSpec(manual)
188
+ if (manualResolved.ready) return { ...base, kind: 'manual', argv: manualResolved.argv, cwd: manualResolved.cwd, ready: true }
189
+ const ext = extensionProviderFor('lua')
190
+ if (ext) return ext
191
+ const candidates = candidateLuaServers(home)
192
+ if (candidates.length) return { ...base, kind: 'discover', argv: [candidates[0]], cwd: undefined, ready: true }
193
+ const inPath = findInPath('lua-language-server' + exeSuffix())
194
+ if (inPath) return { ...base, kind: 'discover', argv: [inPath], ready: true }
195
+ return { ...base, kind: 'none', argv: [], ready: false, reason: manualResolved.reason ?? '未发现 lua-language-server(可安装 LuaLS 或手动指定路径)' }
196
+ }
197
+
198
+ /**
199
+ * 解析 C# provider(优先级:手动 > 扩展源 > 自动发现;需 dotnet + Roslyn dll;或 OmniSharp)。
200
+ * @author ddj 2026年08月27号
201
+ * @param manual 手动配置
202
+ * @param home DSH home(缺省真实;测试可注入临时目录)
203
+ * @returns provider 规格
204
+ */
205
+ export function resolveCSharpProvider(manual?: { command?: string; path?: string }, home = dshHome()): LspProviderSpec {
206
+ const base = { languageId: 'csharp' }
207
+ const manualResolved = manualSpec(manual)
208
+ if (manualResolved.ready) return { ...base, kind: 'manual', argv: manualResolved.argv, cwd: manualResolved.cwd, ready: true }
209
+ const ext = extensionProviderFor('csharp')
210
+ if (ext) return ext
211
+ const discovered = candidateCSharpServers(home)
212
+ if (discovered.roslynDll && discovered.dotnet) {
213
+ return { ...base, kind: 'discover', argv: [discovered.dotnet, discovered.roslynDll, '--stdio'], ready: true }
214
+ }
215
+ if (discovered.omnisharpExe) {
216
+ return { ...base, kind: 'discover', argv: [discovered.omnisharpExe, '--stdio'], ready: true }
217
+ }
218
+ const reasons: string[] = []
219
+ if (!discovered.dotnet) reasons.push('未找到 dotnet')
220
+ if (!discovered.roslynDll && !discovered.omnisharpExe) reasons.push('未发现 ms-dotnettools.csharp 扩展')
221
+ return { ...base, kind: 'none', argv: [], ready: false, reason: reasons.join(';') + '(可手动指定 Roslyn/OmniSharp 入口)' }
222
+ }
223
+
224
+ /** 语言 → provider 解析器(可扩展;Phase 2 由 extmgr 注册扩展源 provider)。 */
225
+ export type ProviderResolver = (manual?: { command?: string; path?: string }) => LspProviderSpec
226
+
227
+ export const PROVIDER_RESOLVERS: Record<string, ProviderResolver> = {
228
+ lua: resolveLuaProvider,
229
+ csharp: resolveCSharpProvider,
230
+ }
231
+
232
+ /** 手动配置是否为绝对路径可执行(供设置页判断输入类型)。 */
233
+ export function looksAbsolute(value: string): boolean {
234
+ return isAbsolute(value)
235
+ }