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.
- package/LICENSE +21 -0
- package/lib/client.js +850 -6
- package/lib/client.js.map +1 -1
- package/lib/index.js +1959 -52
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/events.ts +11 -0
- package/src/client/index.ts +15 -2
- package/src/client/monaco/lsp/index.ts +30 -0
- package/src/client/monaco/lsp/lspClient.ts +192 -0
- package/src/client/monaco/lsp/providers.ts +229 -0
- package/src/client/outline/OutlinePanel.tsx +1 -1
- package/src/client/outline/lspSource.ts +49 -0
- package/src/client/outline/parse.ts +31 -1
- package/src/client/outline/types.ts +2 -0
- package/src/client/styles/editor.css +15 -0
- package/src/client/styles/mcp.css +15 -0
- package/src/client/ui/EditorView.ts +55 -1
- package/src/client/ui/LspSettings.ts +268 -0
- package/src/client/ui/McpSettings.ts +3 -1
- package/src/fileOpenSettings.ts +1 -0
- package/src/index.ts +20 -2
- package/src/lsp/client.ts +205 -0
- package/src/lsp/config.ts +107 -0
- package/src/lsp/extmgr.ts +208 -0
- package/src/lsp/jsonrpc.ts +86 -0
- package/src/lsp/manager.ts +133 -0
- package/src/lsp/providers.ts +235 -0
- package/src/lsp/rpc.ts +334 -0
- package/src/lsp/server.ts +346 -0
- package/src/lsp/settings.ts +44 -0
- package/src/lsp/transport.ts +143 -0
- package/src/lsp/uri.ts +62 -0
- package/src/lsp/zip.ts +82 -0
- package/src/rpc.ts +11 -2
- package/src/shared/lsp.ts +138 -0
- package/src/shared/rpc.ts +33 -0
package/src/lsp/rpc.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — edrv.lsp.* RPC handlers。
|
|
3
|
+
* 文档跟踪(openDocs 集合 + root|lang 引用计数)→ 懒启动服务器、用完释放。
|
|
4
|
+
* 作者 ddj 2026-08-27
|
|
5
|
+
*/
|
|
6
|
+
import type { Ctx } from '../store.js'
|
|
7
|
+
import { searchRoot } from '../search/ripgrep.js'
|
|
8
|
+
import { sessionOf, cwdOf } from '../registry.js'
|
|
9
|
+
import { resolveProviderSpec, langOfPath, configFromPlugin, configFromSettings, LSP_SETTINGS_NS, type LspConfig } from './config.js'
|
|
10
|
+
import type { LspManager } from './manager.js'
|
|
11
|
+
import type { RpcHandlerMap } from '../shared/rpc.js'
|
|
12
|
+
import type { LspLocation } from '../shared/lsp.js'
|
|
13
|
+
import { installFromMarket, installVsixFile, listInstalled, marketGet, marketSearch, uninstall, updateExtension, type ExtInfo } from './extmgr.js'
|
|
14
|
+
|
|
15
|
+
export interface LspRpcDeps {
|
|
16
|
+
ctx: Ctx
|
|
17
|
+
pluginConfig: unknown
|
|
18
|
+
manager: LspManager
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 打开文档跟踪:openDocs(docKey→sessionId) + rootLangRefs 计数 + sessionAcquired 归属。
|
|
23
|
+
* 保证 manager 的 acquire/release 一一配对(多会话共享同一 root|lang 也平衡)。
|
|
24
|
+
*/
|
|
25
|
+
function createDocTracker() {
|
|
26
|
+
const openDocs = new Map<string, Set<string>>()
|
|
27
|
+
const rootLangRefs = new Map<string, number>()
|
|
28
|
+
const sessionDocs = new Map<string, Set<string>>()
|
|
29
|
+
const keyOf = (root: string, lang: string, path: string): string => root + '|' + lang + '|' + path
|
|
30
|
+
const rootLangOf = (docKey: string): string => docKey.slice(0, docKey.lastIndexOf('|'))
|
|
31
|
+
const rememberDoc = (sessionId: string, docKey: string): void => {
|
|
32
|
+
const docs = sessionDocs.get(sessionId) ?? new Set<string>()
|
|
33
|
+
docs.add(docKey)
|
|
34
|
+
sessionDocs.set(sessionId, docs)
|
|
35
|
+
}
|
|
36
|
+
const forgetDoc = (sessionId: string, docKey: string): void => {
|
|
37
|
+
const docs = sessionDocs.get(sessionId)
|
|
38
|
+
docs?.delete(docKey)
|
|
39
|
+
if (docs?.size === 0) sessionDocs.delete(sessionId)
|
|
40
|
+
}
|
|
41
|
+
const releaseRef = (rl: string): boolean => {
|
|
42
|
+
const next = (rootLangRefs.get(rl) ?? 1) - 1
|
|
43
|
+
if (next <= 0) {
|
|
44
|
+
rootLangRefs.delete(rl)
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
rootLangRefs.set(rl, next)
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
/** 记录文档打开;同一文档可由多个会话打开,但每个会话只计一次。 */
|
|
52
|
+
open(root: string, lang: string, path: string, sessionId: string): boolean {
|
|
53
|
+
const docKey = keyOf(root, lang, path)
|
|
54
|
+
const owners = openDocs.get(docKey) ?? new Set<string>()
|
|
55
|
+
if (owners.has(sessionId)) return false
|
|
56
|
+
owners.add(sessionId)
|
|
57
|
+
openDocs.set(docKey, owners)
|
|
58
|
+
rememberDoc(sessionId, docKey)
|
|
59
|
+
const rl = root + '|' + lang
|
|
60
|
+
const count = rootLangRefs.get(rl) ?? 0
|
|
61
|
+
rootLangRefs.set(rl, count + 1)
|
|
62
|
+
return count === 0
|
|
63
|
+
},
|
|
64
|
+
/** 记录文档关闭;返回 true 表示 root|lang 已无文档(应 release)。 */
|
|
65
|
+
close(root: string, lang: string, path: string, sessionId: string): boolean {
|
|
66
|
+
const docKey = keyOf(root, lang, path)
|
|
67
|
+
const owners = openDocs.get(docKey)
|
|
68
|
+
if (!owners?.has(sessionId)) return false
|
|
69
|
+
owners.delete(sessionId)
|
|
70
|
+
if (owners.size === 0) openDocs.delete(docKey)
|
|
71
|
+
forgetDoc(sessionId, docKey)
|
|
72
|
+
return releaseRef(root + '|' + lang)
|
|
73
|
+
},
|
|
74
|
+
/** 关闭某会话全部文档;返回全局引用归零、需要 release 的 root|lang。 */
|
|
75
|
+
closeSession(sessionId: string): string[] {
|
|
76
|
+
const docs = sessionDocs.get(sessionId)
|
|
77
|
+
if (!docs) return []
|
|
78
|
+
const toRelease = new Set<string>()
|
|
79
|
+
for (const docKey of docs) {
|
|
80
|
+
const owners = openDocs.get(docKey)
|
|
81
|
+
owners?.delete(sessionId)
|
|
82
|
+
if (owners?.size === 0) openDocs.delete(docKey)
|
|
83
|
+
if (releaseRef(rootLangOf(docKey))) toRelease.add(rootLangOf(docKey))
|
|
84
|
+
}
|
|
85
|
+
sessionDocs.delete(sessionId)
|
|
86
|
+
return [...toRelease]
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 构造 edrv.lsp.* handlers(一次性创建,tracker 状态跨请求保留)。
|
|
93
|
+
* @author ddj 2026年08月27号
|
|
94
|
+
* @param deps 依赖(ctx/pluginConfig/manager)
|
|
95
|
+
* @returns { handlers, disposeSession } handlers 为局部 map,disposeSession 供 session/disposed 清理
|
|
96
|
+
*/
|
|
97
|
+
export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMap>; disposeSession: (sessionId: string) => void } {
|
|
98
|
+
const { ctx, pluginConfig, manager } = deps
|
|
99
|
+
const tracker = createDocTracker()
|
|
100
|
+
|
|
101
|
+
/** 解析会话与工作区根(失败 → null + error)。 */
|
|
102
|
+
const rootOf = async (sessionId: string | undefined): Promise<{ root: string; sessionId: string } | { err: string }> => {
|
|
103
|
+
const session = sessionOf(ctx, sessionId)
|
|
104
|
+
if (!session) return { err: '会话不存在' }
|
|
105
|
+
const cwd = cwdOf(session)
|
|
106
|
+
if (!cwd) return { err: '会话无工作区' }
|
|
107
|
+
try {
|
|
108
|
+
const root = await searchRoot(ctx, session)
|
|
109
|
+
const sid = typeof session.id === 'string' && session.id ? session.id : sessionId ?? ''
|
|
110
|
+
return { root, sessionId: sid }
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return { err: String(error) }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const acquireServer = (root: string, lang: string) =>
|
|
117
|
+
manager.acquire(root, lang, (languageId: string) => resolveProviderSpec(ctx, pluginConfig, languageId))
|
|
118
|
+
|
|
119
|
+
const serverOf = (root: string, lang: string) =>
|
|
120
|
+
manager.peek(root, lang)
|
|
121
|
+
|
|
122
|
+
const rootLocations = (locations: LspLocation[], root: string): LspLocation[] =>
|
|
123
|
+
locations.map((location) => ({ ...location, root }))
|
|
124
|
+
|
|
125
|
+
const disposeSession = (sessionId: string): void => {
|
|
126
|
+
for (const rl of tracker.closeSession(sessionId)) {
|
|
127
|
+
const sep = rl.lastIndexOf('|')
|
|
128
|
+
manager.release(rl.slice(0, sep), rl.slice(sep + 1))
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const handlers: Partial<RpcHandlerMap> = {
|
|
133
|
+
'edrv.lsp.status': async (args) => {
|
|
134
|
+
if (!args.sessionId) return { ok: true, servers: manager.statusAll() }
|
|
135
|
+
const sc = await rootOf(args.sessionId)
|
|
136
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
137
|
+
return { ok: true, servers: manager.statusAll().filter((status) => status.root === sc.root) }
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
'edrv.lsp.configGet': async () => {
|
|
141
|
+
const settings = configFromSettings(ctx)
|
|
142
|
+
const plugin = configFromPlugin(pluginConfig)
|
|
143
|
+
const merged: LspConfig = { ...plugin }
|
|
144
|
+
for (const lang of Object.keys(settings)) merged[lang] = { ...plugin[lang], ...settings[lang] }
|
|
145
|
+
return { ok: true, config: merged as unknown as Record<string, unknown> }
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
'edrv.lsp.configUpdate': async (args) => {
|
|
149
|
+
const lang = args.languageId
|
|
150
|
+
if (!lang || !resolveProviderSpec(ctx, pluginConfig, lang).languageId) {
|
|
151
|
+
return { ok: false, error: '不支持的语言:' + lang }
|
|
152
|
+
}
|
|
153
|
+
const current = configFromSettings(ctx)
|
|
154
|
+
const next: LspConfig = { ...current, [lang]: { ...(current[lang] ?? {}) } }
|
|
155
|
+
const target = next[lang]!
|
|
156
|
+
if (args.enabled !== undefined) target.enabled = args.enabled
|
|
157
|
+
if (args.command !== undefined) target.command = args.command || undefined
|
|
158
|
+
if (args.path !== undefined) target.path = args.path || undefined
|
|
159
|
+
try {
|
|
160
|
+
const settings = ctx.get('settings')
|
|
161
|
+
if (!settings?.update) return { ok: false, error: '设置服务不可用' }
|
|
162
|
+
await settings.update(LSP_SETTINGS_NS, { [lang]: target })
|
|
163
|
+
const descriptor = settings.describe?.({ redactSecrets: true })?.find((item: { ns?: string }) => item.ns === LSP_SETTINGS_NS)
|
|
164
|
+
const stored = descriptor?.value as Record<string, unknown> | undefined
|
|
165
|
+
const merged: LspConfig = { ...configFromPlugin(pluginConfig), ...(stored as LspConfig) }
|
|
166
|
+
return { ok: true, config: merged as unknown as Record<string, unknown> }
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return { ok: false, error: '配置保存失败:' + String(error) }
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
'edrv.lsp.sync': async (args) => {
|
|
173
|
+
const lang = langOfPath(args.path)
|
|
174
|
+
if (!lang) return { ok: true }
|
|
175
|
+
const sc = await rootOf(args.sessionId)
|
|
176
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
177
|
+
const first = tracker.open(sc.root, lang, args.path, sc.sessionId)
|
|
178
|
+
const server = first ? acquireServer(sc.root, lang) : serverOf(sc.root, lang)
|
|
179
|
+
if (!server) return { ok: false, error: '语言服务器未注册' }
|
|
180
|
+
server.sync(args.path, args.text, args.version)
|
|
181
|
+
return { ok: true }
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
'edrv.lsp.close': async (args) => {
|
|
185
|
+
const lang = langOfPath(args.path)
|
|
186
|
+
if (!lang) return { ok: true }
|
|
187
|
+
const sc = await rootOf(args.sessionId)
|
|
188
|
+
if ('err' in sc) return { ok: true }
|
|
189
|
+
if (tracker.close(sc.root, lang, args.path, sc.sessionId)) {
|
|
190
|
+
manager.release(sc.root, lang)
|
|
191
|
+
}
|
|
192
|
+
return { ok: true }
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
'edrv.lsp.definition': async (args) => {
|
|
196
|
+
const lang = langOfPath(args.path)
|
|
197
|
+
if (!lang) return { ok: true, locations: [] }
|
|
198
|
+
const sc = await rootOf(args.sessionId)
|
|
199
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
200
|
+
try {
|
|
201
|
+
const server = serverOf(sc.root, lang)
|
|
202
|
+
if (!server) return { ok: true, locations: [] }
|
|
203
|
+
const locations = await server.definition(args.path, args.position.line, args.position.character)
|
|
204
|
+
const rooted = rootLocations(locations, sc.root)
|
|
205
|
+
const truncated = rooted.length > 500
|
|
206
|
+
return { ok: true, locations: truncated ? rooted.slice(0, 500) : rooted, truncated: truncated || undefined }
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return { ok: false, error: 'LSP 定义查询失败:' + String(error) }
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
'edrv.lsp.references': async (args) => {
|
|
213
|
+
const lang = langOfPath(args.path)
|
|
214
|
+
if (!lang) return { ok: true, locations: [] }
|
|
215
|
+
const sc = await rootOf(args.sessionId)
|
|
216
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
217
|
+
try {
|
|
218
|
+
const server = serverOf(sc.root, lang)
|
|
219
|
+
if (!server) return { ok: true, locations: [] }
|
|
220
|
+
const locations = await server.references(args.path, args.position.line, args.position.character, args.includeDeclaration !== false)
|
|
221
|
+
const rooted = rootLocations(locations, sc.root)
|
|
222
|
+
const truncated = rooted.length > 500
|
|
223
|
+
return { ok: true, locations: truncated ? rooted.slice(0, 500) : rooted, truncated: truncated || undefined }
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return { ok: false, error: 'LSP 引用查询失败:' + String(error) }
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
'edrv.lsp.documentSymbol': async (args) => {
|
|
230
|
+
const lang = langOfPath(args.path)
|
|
231
|
+
if (!lang) return { ok: true, symbols: [] }
|
|
232
|
+
const sc = await rootOf(args.sessionId)
|
|
233
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
234
|
+
try {
|
|
235
|
+
const server = serverOf(sc.root, lang)
|
|
236
|
+
if (!server) return { ok: true, symbols: [] }
|
|
237
|
+
const symbols = await server.documentSymbol(args.path)
|
|
238
|
+
return { ok: true, symbols }
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return { ok: false, error: 'LSP 大纲查询失败:' + String(error) }
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
'edrv.lsp.workspaceSymbol': async (args) => {
|
|
245
|
+
const sc = await rootOf(args.sessionId)
|
|
246
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
247
|
+
const out: { symbols: never[]; truncated?: boolean } = { symbols: [] }
|
|
248
|
+
for (const lang of ['lua', 'csharp']) {
|
|
249
|
+
const existing = serverOf(sc.root, lang)
|
|
250
|
+
if (!existing) continue
|
|
251
|
+
try {
|
|
252
|
+
const symbols = await existing.workspaceSymbol(args.query)
|
|
253
|
+
if (symbols.length) out.symbols.push(...(symbols as never[]))
|
|
254
|
+
} catch (error) {
|
|
255
|
+
/* 单语言失败忽略 */
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const truncated = out.symbols.length > 500
|
|
259
|
+
return { ok: true, symbols: out.symbols.slice(0, 500), truncated: truncated || undefined }
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
'edrv.lsp.hover': async (args) => {
|
|
263
|
+
const lang = langOfPath(args.path)
|
|
264
|
+
if (!lang) return { ok: true, hover: undefined }
|
|
265
|
+
const sc = await rootOf(args.sessionId)
|
|
266
|
+
if ('err' in sc) return { ok: false, error: sc.err }
|
|
267
|
+
try {
|
|
268
|
+
const server = serverOf(sc.root, lang)
|
|
269
|
+
if (!server) return { ok: true, hover: undefined }
|
|
270
|
+
const hover = await server.hover(args.path, args.position.line, args.position.character)
|
|
271
|
+
return { ok: true, hover: hover ?? undefined }
|
|
272
|
+
} catch (error) {
|
|
273
|
+
return { ok: false, error: 'LSP hover 查询失败:' + String(error) }
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
// --region 扩展管理(edrv.lsp.ext.*:已装/市场/安装/卸载/更新)
|
|
278
|
+
'edrv.lsp.ext.list': async () => ({ ok: true, extensions: listInstalled() }),
|
|
279
|
+
|
|
280
|
+
'edrv.lsp.ext.market': async (args) => {
|
|
281
|
+
try {
|
|
282
|
+
const items = await marketSearch(args.query, args.size ?? 12)
|
|
283
|
+
return { ok: true, extensions: items }
|
|
284
|
+
} catch (error) {
|
|
285
|
+
return { ok: false, error: '市场搜索失败:' + String(error) }
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
'edrv.lsp.ext.install': async (args) => {
|
|
290
|
+
try {
|
|
291
|
+
let info: ExtInfo
|
|
292
|
+
if (args.vsixPath) info = await installVsixFile(args.vsixPath)
|
|
293
|
+
else if (args.namespace && args.name) info = await installFromMarket(args.namespace, args.name, args.version)
|
|
294
|
+
else return { ok: false, error: '缺少安装源(vsixPath 或 namespace+name)' }
|
|
295
|
+
return { ok: true, extension: info }
|
|
296
|
+
} catch (error) {
|
|
297
|
+
return { ok: false, error: '安装失败:' + String(error) }
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
'edrv.lsp.ext.uninstall': async (args) => {
|
|
302
|
+
return uninstall(args.id) ? { ok: true } : { ok: false, error: '扩展未安装:' + args.id }
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
'edrv.lsp.ext.update': async (args) => {
|
|
306
|
+
try {
|
|
307
|
+
const updated = await updateExtension(args.id)
|
|
308
|
+
if (!updated) {
|
|
309
|
+
const current = listInstalled().find((e) => e.id === args.id)
|
|
310
|
+
return current ? { ok: true, extension: current, updated: false } : { ok: false, error: '扩展未安装:' + args.id }
|
|
311
|
+
}
|
|
312
|
+
return { ok: true, extension: updated, updated: true }
|
|
313
|
+
} catch (error) {
|
|
314
|
+
return { ok: false, error: '更新失败:' + String(error) }
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
'edrv.lsp.ext.updates': async () => {
|
|
319
|
+
try {
|
|
320
|
+
const updates = []
|
|
321
|
+
for (const ext of listInstalled()) {
|
|
322
|
+
const meta = await marketGet(ext.namespace, ext.name)
|
|
323
|
+
const latest = String(meta?.version ?? '')
|
|
324
|
+
if (latest && latest !== ext.version) updates.push({ id: ext.id, current: ext.version, latest, displayName: ext.displayName })
|
|
325
|
+
}
|
|
326
|
+
return { ok: true, updates }
|
|
327
|
+
} catch (error) {
|
|
328
|
+
return { ok: false, error: '检查更新失败:' + String(error) }
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
// --endregion
|
|
332
|
+
}
|
|
333
|
+
return { handlers, disposeSession }
|
|
334
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — 单语言服务器会话(绑定 工作区根 + 语言)。
|
|
3
|
+
* 负责:启动传输 + LSP 客户端握手、文档同步(didOpen/didChange/didSave/didClose)、
|
|
4
|
+
* 请求封装(definition/references/documentSymbol/workspaceSymbol/hover)、状态上报。
|
|
5
|
+
* 作者 ddj 2026-08-27
|
|
6
|
+
*/
|
|
7
|
+
import { spawnServer, type Transport } from './transport.js'
|
|
8
|
+
import { createLspClient, type LspClient } from './client.js'
|
|
9
|
+
import { pathToFileUri } from './uri.js'
|
|
10
|
+
import type { LspProviderSpec } from './providers.js'
|
|
11
|
+
import type {
|
|
12
|
+
LspLocation,
|
|
13
|
+
LspServerCapabilities,
|
|
14
|
+
LspServerPhase,
|
|
15
|
+
LspServerStatus,
|
|
16
|
+
LspSymbol,
|
|
17
|
+
} from '../shared/lsp.js'
|
|
18
|
+
|
|
19
|
+
export interface LspServer {
|
|
20
|
+
readonly languageId: string
|
|
21
|
+
readonly root: string
|
|
22
|
+
readonly phase: LspServerPhase
|
|
23
|
+
readonly capabilities: LspServerCapabilities
|
|
24
|
+
onStateChange: ((status: LspServerStatus) => void) | null
|
|
25
|
+
status(): LspServerStatus
|
|
26
|
+
start(): Promise<boolean>
|
|
27
|
+
sync(path: string, text: string, version: number): void
|
|
28
|
+
close(path: string): void
|
|
29
|
+
definition(path: string, line: number, character: number): Promise<LspLocation[]>
|
|
30
|
+
references(path: string, line: number, character: number, includeDeclaration: boolean): Promise<LspLocation[]>
|
|
31
|
+
documentSymbol(path: string): Promise<LspSymbol[]>
|
|
32
|
+
workspaceSymbol(query: string): Promise<LspSymbol[]>
|
|
33
|
+
hover(path: string, line: number, character: number): Promise<{ contents: string[] } | null>
|
|
34
|
+
dispose(): Promise<void>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 文档注册表:path(工作区相对) → { version, text, uri }。 */
|
|
38
|
+
interface OpenDoc {
|
|
39
|
+
path: string
|
|
40
|
+
uri: string
|
|
41
|
+
version: number
|
|
42
|
+
text: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const RESTARTABLE = true
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 创建语言服务器会话。
|
|
49
|
+
* @author ddj 2026年08月27号
|
|
50
|
+
* @param spec provider 规格(argv/cwd)
|
|
51
|
+
* @param root 工作区根(绝对路径)
|
|
52
|
+
* @param languageId 语言 id
|
|
53
|
+
* @param logger 诊断日志
|
|
54
|
+
* @returns 会话句柄
|
|
55
|
+
*/
|
|
56
|
+
export function createLspServer(spec: LspProviderSpec, root: string, languageId: string, logger?: (line: string) => void): LspServer {
|
|
57
|
+
const rootUri = pathToFileUri(root)
|
|
58
|
+
const docs = new Map<string, OpenDoc>()
|
|
59
|
+
let phase: LspServerPhase = 'idle'
|
|
60
|
+
let transport: Transport | null = null
|
|
61
|
+
let client: LspClient | null = null
|
|
62
|
+
let capabilities: LspServerCapabilities = { definition: false, references: false, documentSymbol: false, workspaceSymbol: false, hover: false }
|
|
63
|
+
let disposed = false
|
|
64
|
+
let readyWait: Promise<boolean> = Promise.resolve(false)
|
|
65
|
+
let resolveReady: ((ready: boolean) => void) | null = null
|
|
66
|
+
let lastProgress: { value?: number; message?: string } = {}
|
|
67
|
+
let lastStatus: LspServerStatus = { languageId, source: spec.kind, phase: 'idle', reason: spec.ready ? undefined : spec.reason, root }
|
|
68
|
+
|
|
69
|
+
const log = (line: string): void => logger?.('[' + languageId + '@' + root + '] ' + line)
|
|
70
|
+
|
|
71
|
+
const setPhase = (next: LspServerPhase, reason?: string): void => {
|
|
72
|
+
phase = next
|
|
73
|
+
lastStatus = { languageId, source: spec.kind, phase: next, reason, root, ...progressFields() }
|
|
74
|
+
server.onStateChange?.(lastStatus)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 返回当前进度字段,避免空进度污染状态载荷。 */
|
|
78
|
+
const progressFields = (): Pick<LspServerStatus, 'progress' | 'progressMessage'> => ({
|
|
79
|
+
progress: lastProgress.value,
|
|
80
|
+
progressMessage: lastProgress.message,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
/** 处理 LSP $/progress 通知并映射为编辑器状态。 */
|
|
84
|
+
const applyProgress = (params: unknown): void => {
|
|
85
|
+
const value = params as { value?: unknown }
|
|
86
|
+
const report = value?.value as { kind?: unknown; percentage?: unknown; message?: unknown } | undefined
|
|
87
|
+
if (report?.kind === 'begin') {
|
|
88
|
+
lastProgress = { value: 0, message: typeof report.message === 'string' ? report.message : '正在解析工作区' }
|
|
89
|
+
if (phase === 'ready') phase = 'indexing'
|
|
90
|
+
} else if (report?.kind === 'end') {
|
|
91
|
+
lastProgress = { value: 100, message: typeof report.message === 'string' ? report.message : '解析完成' }
|
|
92
|
+
if (phase === 'indexing') phase = 'ready'
|
|
93
|
+
} else if (typeof report?.percentage === 'number') {
|
|
94
|
+
lastProgress = { value: Math.max(0, Math.min(100, report.percentage)), message: typeof report.message === 'string' ? report.message : lastProgress.message }
|
|
95
|
+
if (phase === 'ready') phase = 'indexing'
|
|
96
|
+
} else if (typeof report?.message === 'string') {
|
|
97
|
+
lastProgress = { ...lastProgress, message: report.message }
|
|
98
|
+
}
|
|
99
|
+
lastStatus = { ...lastStatus, phase, ...progressFields() }
|
|
100
|
+
server.onStateChange?.(lastStatus)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const ensureClient = (): LspClient => {
|
|
104
|
+
if (!client) throw new Error('LSP 服务器未启动')
|
|
105
|
+
return client
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const server: LspServer = {
|
|
109
|
+
languageId,
|
|
110
|
+
root,
|
|
111
|
+
get phase() {
|
|
112
|
+
return phase
|
|
113
|
+
},
|
|
114
|
+
get capabilities() {
|
|
115
|
+
return capabilities
|
|
116
|
+
},
|
|
117
|
+
status(): LspServerStatus {
|
|
118
|
+
return lastStatus
|
|
119
|
+
},
|
|
120
|
+
onStateChange: null,
|
|
121
|
+
|
|
122
|
+
async start(): Promise<boolean> {
|
|
123
|
+
if (disposed) return false
|
|
124
|
+
if (phase === 'starting') return readyWait
|
|
125
|
+
if (phase === 'ready' || phase === 'indexing') return true
|
|
126
|
+
readyWait = new Promise((resolve) => { resolveReady = resolve })
|
|
127
|
+
lastProgress = {}
|
|
128
|
+
setPhase('starting')
|
|
129
|
+
try {
|
|
130
|
+
transport = spawnServer(
|
|
131
|
+
{ argv: spec.argv, cwd: spec.cwd ?? root },
|
|
132
|
+
(message) => { /* 由 client 接管 */ },
|
|
133
|
+
(line) => log('stderr: ' + line),
|
|
134
|
+
)
|
|
135
|
+
client = createLspClient(transport, {
|
|
136
|
+
onReady: (caps) => {
|
|
137
|
+
capabilities = caps
|
|
138
|
+
// 重放已打开文档(崩溃重启后恢复服务器侧状态)
|
|
139
|
+
for (const doc of docs.values()) {
|
|
140
|
+
client?.notify('textDocument/didOpen', {
|
|
141
|
+
textDocument: { uri: doc.uri, languageId, version: doc.version, text: doc.text },
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
setPhase('ready')
|
|
145
|
+
resolveReady?.(true)
|
|
146
|
+
resolveReady = null
|
|
147
|
+
log('ready: definition=' + caps.definition + ' references=' + caps.references + ' symbols=' + caps.documentSymbol)
|
|
148
|
+
},
|
|
149
|
+
onProgress: (params) => applyProgress(params),
|
|
150
|
+
onExit: (code, signal) => {
|
|
151
|
+
if (disposed) return
|
|
152
|
+
resolveReady?.(false)
|
|
153
|
+
resolveReady = null
|
|
154
|
+
log('server exited (' + String(code) + '/' + String(signal) + ')')
|
|
155
|
+
client = null
|
|
156
|
+
transport = null
|
|
157
|
+
if (RESTARTABLE) setPhase('idle', '服务器已退出(' + String(code) + '/' + String(signal) + '),等待重启')
|
|
158
|
+
else setPhase('stopped', '服务器已退出')
|
|
159
|
+
},
|
|
160
|
+
onLog: (line) => log('lsp: ' + line),
|
|
161
|
+
})
|
|
162
|
+
// 让 client 接管 stdout 消息
|
|
163
|
+
const caps = await client.initialize(rootUri, [rootUri])
|
|
164
|
+
capabilities = caps
|
|
165
|
+
return true
|
|
166
|
+
} catch (error) {
|
|
167
|
+
log('start failed: ' + String(error))
|
|
168
|
+
setPhase('unavailable', '启动失败:' + String(error))
|
|
169
|
+
resolveReady?.(false)
|
|
170
|
+
resolveReady = null
|
|
171
|
+
try { await server.dispose() } catch { /* 忽略 */ }
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
sync(path: string, text: string, version: number): void {
|
|
177
|
+
const existing = docs.get(path)
|
|
178
|
+
const uri = existing?.uri ?? pathToFileUri(root + '/' + path.split('/').join('/'))
|
|
179
|
+
if (existing) {
|
|
180
|
+
existing.version = version
|
|
181
|
+
existing.text = text
|
|
182
|
+
if (phase === 'ready' || phase === 'indexing') {
|
|
183
|
+
client?.notify('textDocument/didChange', {
|
|
184
|
+
textDocument: { uri, version },
|
|
185
|
+
contentChanges: [{ text }],
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
docs.set(path, { path, uri, version, text })
|
|
191
|
+
if (phase === 'ready' || phase === 'indexing') {
|
|
192
|
+
client?.notify('textDocument/didOpen', {
|
|
193
|
+
textDocument: { uri, languageId, version, text },
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
close(path: string): void {
|
|
199
|
+
const doc = docs.get(path)
|
|
200
|
+
if (!doc) return
|
|
201
|
+
docs.delete(path)
|
|
202
|
+
if (phase === 'ready' || phase === 'indexing') {
|
|
203
|
+
client?.notify('textDocument/didClose', { textDocument: { uri: doc.uri } })
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
async definition(path: string, line: number, character: number): Promise<LspLocation[]> {
|
|
208
|
+
const doc = docs.get(path)
|
|
209
|
+
if (!doc || !(await readyWait)) return []
|
|
210
|
+
const result = await ensureClient().request<unknown>(
|
|
211
|
+
'textDocument/definition',
|
|
212
|
+
{ textDocument: { uri: doc.uri }, position: { line, character } },
|
|
213
|
+
)
|
|
214
|
+
return normalizeLocations(result)
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
async references(path: string, line: number, character: number, includeDeclaration: boolean): Promise<LspLocation[]> {
|
|
218
|
+
const doc = docs.get(path)
|
|
219
|
+
if (!doc || !(await readyWait)) return []
|
|
220
|
+
const result = await ensureClient().request<unknown>(
|
|
221
|
+
'textDocument/references',
|
|
222
|
+
{
|
|
223
|
+
textDocument: { uri: doc.uri },
|
|
224
|
+
position: { line, character },
|
|
225
|
+
context: { includeDeclaration },
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
return normalizeLocations(result)
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
async documentSymbol(path: string): Promise<LspSymbol[]> {
|
|
232
|
+
const doc = docs.get(path)
|
|
233
|
+
if (!doc || !(await readyWait)) return []
|
|
234
|
+
const result = await ensureClient().request<unknown>('textDocument/documentSymbol', { textDocument: { uri: doc.uri } })
|
|
235
|
+
if (!Array.isArray(result)) return []
|
|
236
|
+
return (result as unknown[]).map(normalizeSymbol).filter((s): s is LspSymbol => s !== null)
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
async workspaceSymbol(query: string): Promise<LspSymbol[]> {
|
|
240
|
+
if (!(await readyWait)) return []
|
|
241
|
+
const result = await ensureClient().request<unknown>('workspace/symbol', { query })
|
|
242
|
+
if (!Array.isArray(result)) return []
|
|
243
|
+
return (result as unknown[]).map(normalizeSymbol).filter((s): s is LspSymbol => s !== null)
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
async hover(path: string, line: number, character: number): Promise<{ contents: string[] } | null> {
|
|
247
|
+
const doc = docs.get(path)
|
|
248
|
+
if (!doc || !(await readyWait)) return null
|
|
249
|
+
const result = await ensureClient().request<{ contents?: unknown; range?: unknown } | null>(
|
|
250
|
+
'textDocument/hover',
|
|
251
|
+
{ textDocument: { uri: doc.uri }, position: { line, character } },
|
|
252
|
+
)
|
|
253
|
+
if (!result || !result.contents) return null
|
|
254
|
+
return { contents: stringifyHoverContents(result.contents) }
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
async dispose(): Promise<void> {
|
|
258
|
+
disposed = true
|
|
259
|
+
resolveReady?.(false)
|
|
260
|
+
resolveReady = null
|
|
261
|
+
if (client) {
|
|
262
|
+
await client.shutdown().catch(() => {})
|
|
263
|
+
client = null
|
|
264
|
+
}
|
|
265
|
+
if (transport) {
|
|
266
|
+
transport.dispose()
|
|
267
|
+
transport = null
|
|
268
|
+
}
|
|
269
|
+
setPhase('stopped')
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
return server
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** 归一化 definition/references 结果(Location | Location[] | LocationLink[])。 */
|
|
276
|
+
function normalizeLocations(result: unknown): LspLocation[] {
|
|
277
|
+
if (!Array.isArray(result)) return []
|
|
278
|
+
const out: LspLocation[] = []
|
|
279
|
+
for (const item of result) {
|
|
280
|
+
if (!item || typeof item !== 'object') continue
|
|
281
|
+
const obj = item as { uri?: unknown; range?: unknown; targetUri?: unknown; targetRange?: unknown; targetSelectionRange?: unknown }
|
|
282
|
+
const uri = typeof obj.targetUri === 'string' ? obj.targetUri : typeof obj.uri === 'string' ? obj.uri : ''
|
|
283
|
+
const range = (obj.targetRange ?? obj.range) as { start?: { line?: unknown; character?: unknown }; end?: { line?: unknown; character?: unknown } } | undefined
|
|
284
|
+
if (!uri || !range || !range.start || !range.end) continue
|
|
285
|
+
out.push({
|
|
286
|
+
uri,
|
|
287
|
+
range: {
|
|
288
|
+
start: { line: toInt(range.start.line), character: toInt(range.start.character) },
|
|
289
|
+
end: { line: toInt(range.end.line), character: toInt(range.end.character) },
|
|
290
|
+
},
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
return out
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** 归一化 documentSymbol/workspaceSymbol 结果(DocumentSymbol | SymbolInformation)。 */
|
|
297
|
+
function normalizeSymbol(item: unknown): LspSymbol | null {
|
|
298
|
+
if (!item || typeof item !== 'object') return null
|
|
299
|
+
const obj = item as {
|
|
300
|
+
name?: unknown
|
|
301
|
+
kind?: unknown
|
|
302
|
+
detail?: unknown
|
|
303
|
+
containerName?: unknown
|
|
304
|
+
range?: { start?: unknown; end?: unknown }
|
|
305
|
+
selectionRange?: { start?: unknown; end?: unknown }
|
|
306
|
+
children?: unknown
|
|
307
|
+
location?: { uri?: unknown; range?: unknown }
|
|
308
|
+
}
|
|
309
|
+
if (typeof obj.name !== 'string') return null
|
|
310
|
+
const selection = obj.selectionRange ?? (obj.location && (obj.location as { range?: unknown }).range)
|
|
311
|
+
const range = obj.range ?? selection
|
|
312
|
+
if (!range || typeof range !== 'object') return null
|
|
313
|
+
const r = range as { start?: { line?: unknown; character?: unknown }; end?: { line?: unknown; character?: unknown } }
|
|
314
|
+
if (!r.start || !r.end) return null
|
|
315
|
+
const symbol: LspSymbol = {
|
|
316
|
+
name: obj.name,
|
|
317
|
+
kind: toInt(obj.kind),
|
|
318
|
+
range: { start: { line: toInt((r.start as { line?: unknown }).line), character: toInt((r.start as { character?: unknown }).character) }, end: { line: toInt((r.end as { line?: unknown }).line), character: toInt((r.end as { character?: unknown }).character) } },
|
|
319
|
+
selectionRange: selection
|
|
320
|
+
? { start: { line: toInt(((selection as { start?: { line?: unknown } }).start as { line?: unknown } | undefined)?.line ?? toInt((r.start as { line?: unknown }).line)), character: toInt(((selection as { start?: { character?: unknown } }).start as { character?: unknown } | undefined)?.character ?? toInt((r.start as { character?: unknown }).character)) }, end: { line: toInt(((selection as { end?: { line?: unknown } }).end as { line?: unknown } | undefined)?.line ?? toInt((r.end as { line?: unknown }).line)), character: toInt(((selection as { end?: { character?: unknown } }).end as { character?: unknown } | undefined)?.character ?? toInt((r.end as { character?: unknown }).character)) } }
|
|
321
|
+
: { start: { line: toInt((r.start as { line?: unknown }).line), character: toInt((r.start as { character?: unknown }).character) }, end: { line: toInt((r.end as { line?: unknown }).line), character: toInt((r.end as { character?: unknown }).character) } },
|
|
322
|
+
}
|
|
323
|
+
if (typeof obj.detail === 'string' && obj.detail) symbol.detail = obj.detail
|
|
324
|
+
if (typeof obj.containerName === 'string' && obj.containerName) symbol.containerName = obj.containerName
|
|
325
|
+
if (Array.isArray(obj.children)) {
|
|
326
|
+
const kids = obj.children.map(normalizeSymbol).filter((s): s is LspSymbol => s !== null)
|
|
327
|
+
if (kids.length) symbol.children = kids
|
|
328
|
+
}
|
|
329
|
+
return symbol
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** hover contents → 文本行(MarkupContent | MarkedString | MarkedString[])。 */
|
|
333
|
+
function stringifyHoverContents(contents: unknown): string[] {
|
|
334
|
+
if (typeof contents === 'string') return [contents]
|
|
335
|
+
if (Array.isArray(contents)) return contents.map(stringifyHoverContents).flat().filter(Boolean)
|
|
336
|
+
if (contents && typeof contents === 'object') {
|
|
337
|
+
const value = (contents as { value?: unknown }).value
|
|
338
|
+
if (typeof value === 'string') return [value]
|
|
339
|
+
}
|
|
340
|
+
return []
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function toInt(value: unknown): number {
|
|
344
|
+
const n = Number(value)
|
|
345
|
+
return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0
|
|
346
|
+
}
|