dsh-vscode-mode 0.1.30 → 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/README.md +30 -0
- package/lib/client.js +1439 -46
- package/lib/client.js.map +1 -1
- package/lib/index.js +3519 -222
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/capture.ts +3 -0
- package/src/client/events.ts +11 -0
- package/src/client/index.ts +20 -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/paths.ts +36 -0
- package/src/client/sidebar/panels/FileExplorer.ts +95 -30
- package/src/client/sidebar/panels/SearchPanel.ts +11 -2
- package/src/client/state/explorerCache.ts +3 -1
- package/src/client/state/explorerEntriesCache.ts +172 -0
- package/src/client/state/viewStateCache.ts +3 -1
- package/src/client/styles/editor.css +16 -0
- package/src/client/styles/mcp.css +63 -0
- package/src/client/ui/EditorView.ts +61 -6
- package/src/client/ui/LspSettings.ts +268 -0
- package/src/client/ui/McpSettings.ts +5 -1
- package/src/client/ui/PerfSettings.ts +242 -0
- package/src/client/ui/PerfSizeBadge.ts +54 -0
- package/src/compat.ts +3 -2
- package/src/fileOpenSettings.ts +1 -0
- package/src/index.ts +25 -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/paths.ts +284 -0
- package/src/perf.ts +340 -0
- package/src/perfPatch.ts +76 -0
- package/src/routes.ts +7 -12
- package/src/rpc.ts +184 -36
- package/src/search/content.ts +14 -7
- package/src/search/ripgrep.ts +54 -1
- package/src/search/types.ts +4 -1
- package/src/shared/lsp.ts +138 -0
- package/src/shared/rpc.ts +103 -2
- package/src/store.ts +2 -2
- package/src/treeIndex.ts +397 -0
- package/src/treeRead.ts +87 -0
|
@@ -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
|
+
}
|
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
|
+
}
|