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
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — LSP 客户端(JSON-RPC over stdio)。
|
|
3
|
+
* 职责:请求/通知/握手/超时/服务器端请求处理/生命周期。transport 无关性:
|
|
4
|
+
* 只依赖 write/onMessage 契约,便于单测时注入假传输。
|
|
5
|
+
* 作者 ddj 2026-08-27
|
|
6
|
+
*/
|
|
7
|
+
import { createFrameParser, encodeMessage, requestMessage, notifyMessage } from './jsonrpc.js'
|
|
8
|
+
import type { LspServerCapabilities } from '../shared/lsp.js'
|
|
9
|
+
|
|
10
|
+
export interface LspClientEvents {
|
|
11
|
+
onReady?: (capabilities: LspServerCapabilities) => void
|
|
12
|
+
onExit?: (code: number | null, signal: string | null) => void
|
|
13
|
+
onLog?: (line: string) => void
|
|
14
|
+
onProgress?: (params: unknown) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_TIMEOUT_MS = 20_000
|
|
18
|
+
|
|
19
|
+
export class LspError extends Error {
|
|
20
|
+
code: number
|
|
21
|
+
constructor(code: number, message: string) {
|
|
22
|
+
super(message)
|
|
23
|
+
this.code = code
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface LspClientTransport {
|
|
28
|
+
write(chunk: Buffer): boolean
|
|
29
|
+
dispose(): void
|
|
30
|
+
readonly alive: boolean
|
|
31
|
+
onMessage: ((message: unknown) => void) | null
|
|
32
|
+
onExit: ((code: number | null, signal: string | null) => void) | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface LspClient {
|
|
36
|
+
request<T>(method: string, params: unknown, timeoutMs?: number): Promise<T>
|
|
37
|
+
notify(method: string, params: unknown): void
|
|
38
|
+
initialize(rootUri: string, workspaceFolders: string[]): Promise<LspServerCapabilities>
|
|
39
|
+
shutdown(): Promise<void>
|
|
40
|
+
readonly alive: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 创建 LSP 客户端。
|
|
45
|
+
* @author ddj 2026年08月27号
|
|
46
|
+
* @param transport 传输句柄(write/onMessage/alive/dispose)
|
|
47
|
+
* @param events 事件回调
|
|
48
|
+
* @returns LSP 客户端
|
|
49
|
+
*/
|
|
50
|
+
export function createLspClient(
|
|
51
|
+
transport: LspClientTransport,
|
|
52
|
+
events: LspClientEvents = {},
|
|
53
|
+
): LspClient {
|
|
54
|
+
let nextId = 1
|
|
55
|
+
let alive = transport.alive
|
|
56
|
+
const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }>()
|
|
57
|
+
let serverCapabilities: LspServerCapabilities = { definition: false, references: false, documentSymbol: false, workspaceSymbol: false, hover: false }
|
|
58
|
+
|
|
59
|
+
const log = (line: string): void => events.onLog?.(line)
|
|
60
|
+
const parser = createFrameParser()
|
|
61
|
+
|
|
62
|
+
transport.onMessage = (message: unknown): void => {
|
|
63
|
+
const msg = message as { id?: unknown; method?: unknown; params?: unknown; result?: unknown; error?: unknown }
|
|
64
|
+
// 服务器端请求/通知带 method(请求还带 id);响应带 id + result/error、无 method。先判 method 再判 id。
|
|
65
|
+
if (typeof msg.method === 'string') {
|
|
66
|
+
if (msg.method === '$/progress') events.onProgress?.(msg.params)
|
|
67
|
+
void handleServerRequest(msg as { method: string; params?: unknown; id?: unknown })
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
if (typeof msg.id === 'number' || typeof msg.id === 'string') {
|
|
71
|
+
const entry = pending.get(Number(msg.id))
|
|
72
|
+
if (!entry) return
|
|
73
|
+
pending.delete(Number(msg.id))
|
|
74
|
+
clearTimeout(entry.timer)
|
|
75
|
+
if (msg.error) {
|
|
76
|
+
const err = msg.error as { code?: number; message?: string }
|
|
77
|
+
entry.reject(new LspError(err.code ?? -32000, err.message ?? 'LSP error'))
|
|
78
|
+
} else {
|
|
79
|
+
entry.resolve(msg.result)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 服务器端请求:消息/日志类打日志,配置/能力类回 result:null,其余回 method not found。 */
|
|
85
|
+
const handleServerRequest = async (msg: { method: string; params?: unknown; id?: unknown }): Promise<void> => {
|
|
86
|
+
const { method, params, id } = msg
|
|
87
|
+
const respond = (payload: Record<string, unknown>): void => {
|
|
88
|
+
transport.write(encodeMessage(payload))
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
if (method === 'window/logMessage' || method === 'window/showMessage' || method === 'window/showMessageRequest') {
|
|
92
|
+
const p = params as { type?: number; message?: string }
|
|
93
|
+
log('[server ' + (p?.type ?? '') + '] ' + String(p?.message ?? ''))
|
|
94
|
+
if (id !== undefined) respond({ jsonrpc: '2.0', id, result: null })
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
if (method === 'workspace/configuration') {
|
|
98
|
+
const p = params as { items?: unknown[] }
|
|
99
|
+
const items = Array.isArray(p?.items) ? p.items.map(() => null) : []
|
|
100
|
+
if (id !== undefined) respond({ jsonrpc: '2.0', id, result: items })
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
// 客户端能力协商类:标准行为是确认(result:null),否则部分服务器(如 LuaLS)会中断工作区初始化
|
|
104
|
+
if (method === 'window/workDoneProgress/create' || method === 'client/registerCapability' || method === 'workspace/workspaceFolders') {
|
|
105
|
+
if (id !== undefined) respond({ jsonrpc: '2.0', id, result: null })
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
if (id !== undefined) {
|
|
109
|
+
respond({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found: ' + method } })
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
log('server request handler error: ' + String(error))
|
|
113
|
+
if (id !== undefined) {
|
|
114
|
+
respond({ jsonrpc: '2.0', id, error: { code: -32603, message: String(error) } })
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const transportExit = transport.onExit
|
|
120
|
+
transport.onExit = (code, signal): void => {
|
|
121
|
+
alive = false
|
|
122
|
+
for (const entry of pending.values()) {
|
|
123
|
+
clearTimeout(entry.timer)
|
|
124
|
+
entry.reject(new Error('LSP 服务器已退出(' + String(code) + '/' + String(signal) + ')'))
|
|
125
|
+
}
|
|
126
|
+
pending.clear()
|
|
127
|
+
events.onExit?.(code, signal)
|
|
128
|
+
transportExit?.(code, signal)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const client: LspClient = {
|
|
132
|
+
async request<T>(method: string, params: unknown, timeoutMs = DEFAULT_TIMEOUT_MS): Promise<T> {
|
|
133
|
+
if (!alive) throw new Error('LSP 服务器未运行')
|
|
134
|
+
const id = nextId++
|
|
135
|
+
return new Promise<T>((resolve, reject) => {
|
|
136
|
+
const timer = setTimeout(() => {
|
|
137
|
+
pending.delete(id)
|
|
138
|
+
reject(new Error('LSP 请求超时:' + method + '(' + timeoutMs + 'ms)'))
|
|
139
|
+
}, timeoutMs)
|
|
140
|
+
pending.set(id, { resolve: resolve as (v: unknown) => void, reject, timer })
|
|
141
|
+
transport.write(encodeMessage(requestMessage(id, method, params)))
|
|
142
|
+
})
|
|
143
|
+
},
|
|
144
|
+
notify(method: string, params: unknown): void {
|
|
145
|
+
if (!alive) return
|
|
146
|
+
transport.write(encodeMessage(notifyMessage(method, params)))
|
|
147
|
+
},
|
|
148
|
+
async initialize(rootUri: string, workspaceFolders: string[]): Promise<LspServerCapabilities> {
|
|
149
|
+
const init = await client.request<{
|
|
150
|
+
capabilities?: {
|
|
151
|
+
definitionProvider?: unknown
|
|
152
|
+
referencesProvider?: unknown
|
|
153
|
+
documentSymbolProvider?: unknown
|
|
154
|
+
workspaceSymbolProvider?: unknown
|
|
155
|
+
hoverProvider?: unknown
|
|
156
|
+
}
|
|
157
|
+
}>('initialize', {
|
|
158
|
+
processId: process.pid,
|
|
159
|
+
rootUri: rootUri || null,
|
|
160
|
+
rootPath: null,
|
|
161
|
+
workspaceFolders: workspaceFolders.length
|
|
162
|
+
? workspaceFolders.map((uri) => ({ uri, name: uri }))
|
|
163
|
+
: null,
|
|
164
|
+
capabilities: {
|
|
165
|
+
window: { workDoneProgress: true },
|
|
166
|
+
workspace: { configuration: true },
|
|
167
|
+
textDocument: {
|
|
168
|
+
synchronization: { dynamicRegistration: false, willSave: false, didSave: true },
|
|
169
|
+
definition: { dynamicRegistration: false },
|
|
170
|
+
references: { dynamicRegistration: false },
|
|
171
|
+
documentSymbol: { dynamicRegistration: false },
|
|
172
|
+
hover: { dynamicRegistration: false },
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
const caps = init?.capabilities ?? {}
|
|
177
|
+
serverCapabilities = {
|
|
178
|
+
definition: Boolean(caps.definitionProvider),
|
|
179
|
+
references: Boolean(caps.referencesProvider),
|
|
180
|
+
documentSymbol: Boolean(caps.documentSymbolProvider),
|
|
181
|
+
workspaceSymbol: Boolean(caps.workspaceSymbolProvider),
|
|
182
|
+
hover: Boolean(caps.hoverProvider),
|
|
183
|
+
}
|
|
184
|
+
client.notify('initialized', {})
|
|
185
|
+
events.onReady?.(serverCapabilities)
|
|
186
|
+
return serverCapabilities
|
|
187
|
+
},
|
|
188
|
+
async shutdown(): Promise<void> {
|
|
189
|
+
try {
|
|
190
|
+
await client.request<unknown>('shutdown', null, 3000)
|
|
191
|
+
} catch (error) {
|
|
192
|
+
log('shutdown: ' + String(error))
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
client.notify('exit', null)
|
|
196
|
+
} catch (error) {
|
|
197
|
+
log('exit: ' + String(error))
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
get alive() {
|
|
201
|
+
return alive
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
return client
|
|
205
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — LSP 配置读取与 provider 解析装配。
|
|
3
|
+
* 配置来源优先级:settings 持久化(ns dsh-vscode-mode.languageServers)> 插件组合配置 > 默认。
|
|
4
|
+
* 作者 ddj 2026-08-27
|
|
5
|
+
*/
|
|
6
|
+
import type { Ctx } from '../store.js'
|
|
7
|
+
import { PROVIDER_RESOLVERS, type LspProviderSpec } from './providers.js'
|
|
8
|
+
|
|
9
|
+
/** 单语言配置。 */
|
|
10
|
+
export interface LspLangConfig {
|
|
11
|
+
enabled?: boolean
|
|
12
|
+
command?: string
|
|
13
|
+
path?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** LSP 配置(按语言)。 */
|
|
17
|
+
export interface LspConfig {
|
|
18
|
+
lua?: LspLangConfig
|
|
19
|
+
csharp?: LspLangConfig
|
|
20
|
+
[languageId: string]: LspLangConfig | undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const LSP_SETTINGS_NS = 'dsh-vscode-mode.languageServers'
|
|
24
|
+
export const LSP_LANGUAGES = ['lua', 'csharp'] as const
|
|
25
|
+
|
|
26
|
+
function sanitizeLang(raw: unknown): LspLangConfig | undefined {
|
|
27
|
+
if (!raw || typeof raw !== 'object') return undefined
|
|
28
|
+
const obj = raw as Record<string, unknown>
|
|
29
|
+
const out: LspLangConfig = {}
|
|
30
|
+
if (typeof obj.enabled === 'boolean') out.enabled = obj.enabled
|
|
31
|
+
if (typeof obj.command === 'string') out.command = obj.command
|
|
32
|
+
if (typeof obj.path === 'string') out.path = obj.path
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 从插件组合配置读取。 */
|
|
37
|
+
export function configFromPlugin(config: unknown): LspConfig {
|
|
38
|
+
const raw = (config as { languageServers?: unknown } | undefined)?.languageServers
|
|
39
|
+
if (!raw || typeof raw !== 'object') return {}
|
|
40
|
+
const out: LspConfig = {}
|
|
41
|
+
for (const lang of LSP_LANGUAGES) {
|
|
42
|
+
const v = sanitizeLang((raw as Record<string, unknown>)[lang])
|
|
43
|
+
if (v) out[lang] = v
|
|
44
|
+
}
|
|
45
|
+
return out
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 从 settings 描述符读取(无 settings 服务/无该 section → {})。 */
|
|
49
|
+
export function configFromSettings(ctx: Ctx): LspConfig {
|
|
50
|
+
try {
|
|
51
|
+
const settings = ctx.get('settings')
|
|
52
|
+
const descriptor = settings?.describe?.({ redactSecrets: true })?.find((item: { ns?: string }) => item.ns === LSP_SETTINGS_NS)
|
|
53
|
+
const value = descriptor?.value
|
|
54
|
+
if (!value || typeof value !== 'object') return {}
|
|
55
|
+
const out: LspConfig = {}
|
|
56
|
+
for (const lang of LSP_LANGUAGES) {
|
|
57
|
+
const v = sanitizeLang((value as Record<string, unknown>)[lang])
|
|
58
|
+
if (v) out[lang] = v
|
|
59
|
+
}
|
|
60
|
+
return out
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return {}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 合并配置:settings 覆盖插件配置。 */
|
|
67
|
+
export function mergeConfig(a: LspConfig, b: LspConfig): LspConfig {
|
|
68
|
+
const out: LspConfig = { ...a }
|
|
69
|
+
for (const lang of LSP_LANGUAGES) {
|
|
70
|
+
const av = a[lang]
|
|
71
|
+
const bv = b[lang]
|
|
72
|
+
if (bv) out[lang] = { ...av, ...bv }
|
|
73
|
+
else if (av) out[lang] = av
|
|
74
|
+
}
|
|
75
|
+
return out
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 解析某语言的 provider spec(合并配置后)。
|
|
80
|
+
* @author ddj 2026年08月27号
|
|
81
|
+
* @param ctx DSH 上下文
|
|
82
|
+
* @param pluginConfig 插件组合配置
|
|
83
|
+
* @param languageId 语言 id
|
|
84
|
+
* @returns provider 规格
|
|
85
|
+
*/
|
|
86
|
+
export function resolveProviderSpec(ctx: Ctx, pluginConfig: unknown, languageId: string): LspProviderSpec {
|
|
87
|
+
const resolver = PROVIDER_RESOLVERS[languageId]
|
|
88
|
+
if (!resolver) return { languageId, kind: 'none', argv: [], ready: false, reason: '不支持的语言:' + languageId }
|
|
89
|
+
const merged = mergeConfig(configFromPlugin(pluginConfig), configFromSettings(ctx))
|
|
90
|
+
const lang = merged[languageId]
|
|
91
|
+
if (lang && lang.enabled === false) {
|
|
92
|
+
return { languageId, kind: 'none', argv: [], ready: false, reason: '已在设置中禁用' }
|
|
93
|
+
}
|
|
94
|
+
return resolver({ command: lang?.command, path: lang?.path })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 语言 → 扩展名(供客户端判断是否触发 LSP)。 */
|
|
98
|
+
export function langOfPath(path: string): string | null {
|
|
99
|
+
const base = String(path || '').split(/[\\/]/).pop() || ''
|
|
100
|
+
const dot = base.lastIndexOf('.')
|
|
101
|
+
if (dot <= 0) return null
|
|
102
|
+
const ext = base.slice(dot + 1).toLowerCase()
|
|
103
|
+
for (const [lang, exts] of Object.entries({ lua: ['lua'], csharp: ['cs', 'csx'] })) {
|
|
104
|
+
if (exts.includes(ext)) return lang
|
|
105
|
+
}
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — VSIX 扩展管理器(extmgr)。
|
|
3
|
+
* 管理 DSH 专属扩展目录(~/.dsh/dsh-vscode-mode/extensions/),支持:
|
|
4
|
+
* 本地 .vsix 安装、Open VSX 市场搜索/下载安装、卸载、更新检测与安装。
|
|
5
|
+
* 已装扩展作为 LSP provider 的"扩展源"(kind=extension),见 providers.ts。
|
|
6
|
+
* 作者 ddj 2026-08-27
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
9
|
+
import { basename, dirname, join } from 'node:path'
|
|
10
|
+
import { unzip } from './zip.js'
|
|
11
|
+
import { dshHome, PLUGIN_ID } from '../paths.js'
|
|
12
|
+
|
|
13
|
+
/** 单个已装扩展信息(edrv.lsp.ext.list 载荷元素)。 */
|
|
14
|
+
export interface ExtInfo {
|
|
15
|
+
id: string // publisher.name
|
|
16
|
+
namespace: string
|
|
17
|
+
name: string
|
|
18
|
+
version: string
|
|
19
|
+
displayName?: string
|
|
20
|
+
description?: string
|
|
21
|
+
dirName: string // 目录名 publisher.name-version
|
|
22
|
+
dir: string // 绝对路径
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Open VSX 市场搜索结果(edrv.lsp.ext.market 载荷元素)。 */
|
|
26
|
+
export interface MarketItem {
|
|
27
|
+
namespace: string
|
|
28
|
+
name: string
|
|
29
|
+
id: string
|
|
30
|
+
version: string
|
|
31
|
+
displayName?: string
|
|
32
|
+
description?: string
|
|
33
|
+
download?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const OPEN_VSX = 'https://open-vsx.org'
|
|
37
|
+
const META_FILE = 'package.json'
|
|
38
|
+
/** 当前平台(与 VSCode platform 命名对齐:win32-x64/darwin-arm64/linux-x64)。 */
|
|
39
|
+
const PLATFORM = (() => {
|
|
40
|
+
if (process.platform === 'win32') return process.arch === 'arm64' ? 'win32-arm64' : 'win32-x64'
|
|
41
|
+
if (process.platform === 'darwin') return process.arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64'
|
|
42
|
+
return process.arch === 'arm64' ? 'linux-arm64' : 'linux-x64'
|
|
43
|
+
})()
|
|
44
|
+
|
|
45
|
+
/** DSH 专属扩展目录。 */
|
|
46
|
+
export function extensionsRoot(home = dshHome()): string {
|
|
47
|
+
return join(home, PLUGIN_ID, 'extensions')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 从 VSIX 字节里读 extension/package.json 清单。 */
|
|
51
|
+
export function vsixManifest(vsix: Buffer): Record<string, unknown> {
|
|
52
|
+
for (const entry of unzip(vsix)) {
|
|
53
|
+
if (entry.path === 'extension/package.json' || entry.path === 'extension\\package.json') {
|
|
54
|
+
return JSON.parse(entry.data.toString('utf8')) as Record<string, unknown>
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
throw new Error('VSIX 缺少 extension/package.json')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 解包 VSIX 到目录(仅取 extension/ 子树,剥前缀;返回清单)。
|
|
62
|
+
* @author ddj 2026年08月27号
|
|
63
|
+
* @param vsix vsix 字节
|
|
64
|
+
* @param dest 目标目录(已存在则先清空)
|
|
65
|
+
* @returns 清单对象
|
|
66
|
+
*/
|
|
67
|
+
export function unpackVsix(vsix: Buffer, dest: string): Record<string, unknown> {
|
|
68
|
+
rmSync(dest, { recursive: true, force: true })
|
|
69
|
+
mkdirSync(dest, { recursive: true })
|
|
70
|
+
let manifest: Record<string, unknown> | null = null
|
|
71
|
+
for (const entry of unzip(vsix)) {
|
|
72
|
+
const rel = entry.path.replace(/^extension[\\/]/, '')
|
|
73
|
+
if (!rel || entry.path.startsWith('..')) continue
|
|
74
|
+
const safe = rel.split(/[\\/]/).filter((part) => part && part !== '..' && part !== '.').join('/')
|
|
75
|
+
if (!safe) continue
|
|
76
|
+
const target = join(dest, ...safe.split('/'))
|
|
77
|
+
if (entry.isDirectory) {
|
|
78
|
+
mkdirSync(target, { recursive: true })
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
82
|
+
writeFileSync(target, entry.data)
|
|
83
|
+
if (rel === 'package.json') manifest = JSON.parse(entry.data.toString('utf8')) as Record<string, unknown>
|
|
84
|
+
}
|
|
85
|
+
if (!manifest) throw new Error('VSIX 解包后缺少 package.json')
|
|
86
|
+
return manifest
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 扩展信息(清单 + 目录)。 */
|
|
90
|
+
export function extInfoOf(manifest: Record<string, unknown>, dir: string): ExtInfo {
|
|
91
|
+
const namespace = String(manifest.publisher ?? 'unknown')
|
|
92
|
+
const name = String(manifest.name ?? 'unknown')
|
|
93
|
+
const version = String(manifest.version ?? '0.0.0')
|
|
94
|
+
return {
|
|
95
|
+
id: namespace + '.' + name,
|
|
96
|
+
namespace,
|
|
97
|
+
name,
|
|
98
|
+
version,
|
|
99
|
+
displayName: typeof manifest.displayName === 'string' ? manifest.displayName : undefined,
|
|
100
|
+
description: typeof manifest.description === 'string' ? manifest.description : undefined,
|
|
101
|
+
dirName: basename(dir),
|
|
102
|
+
dir,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 安装本地 .vsix 文件(已存在同 id 则升级替换)。 */
|
|
107
|
+
export async function installVsixFile(vsixPath: string, home = dshHome()): Promise<ExtInfo> {
|
|
108
|
+
const vsix = readFileSync(vsixPath)
|
|
109
|
+
return installVsixBuffer(vsix, home)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 安装 VSIX 字节(解包进 extensionsRoot;同名旧版本先清)。 */
|
|
113
|
+
export function installVsixBuffer(vsix: Buffer, home = dshHome()): ExtInfo {
|
|
114
|
+
const manifest = vsixManifest(vsix)
|
|
115
|
+
const info = extInfoOf(manifest, '')
|
|
116
|
+
const target = join(extensionsRoot(home), info.id + '-' + info.version)
|
|
117
|
+
const unpacked = unpackVsix(vsix, target)
|
|
118
|
+
return extInfoOf(unpacked, target)
|
|
119
|
+
}
|
|
120
|
+
/** 扫描已装扩展目录。 */
|
|
121
|
+
export function listInstalled(home = dshHome()): ExtInfo[] {
|
|
122
|
+
const root = extensionsRoot(home)
|
|
123
|
+
if (!existsSync(root)) return []
|
|
124
|
+
const out: ExtInfo[] = []
|
|
125
|
+
for (const dir of readdirSync(root)) {
|
|
126
|
+
const full = join(root, dir)
|
|
127
|
+
const meta = join(full, META_FILE)
|
|
128
|
+
if (!existsSync(meta)) continue
|
|
129
|
+
try {
|
|
130
|
+
const manifest = JSON.parse(readFileSync(meta, 'utf8')) as Record<string, unknown>
|
|
131
|
+
out.push(extInfoOf(manifest, full))
|
|
132
|
+
} catch (error) {
|
|
133
|
+
/* 坏清单目录跳过 */
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return out
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 卸载扩展(按 id)。 */
|
|
140
|
+
export function uninstall(id: string, home = dshHome()): boolean {
|
|
141
|
+
const target = listInstalled(home).find((e) => e.id === id)?.dir
|
|
142
|
+
if (!target) return false
|
|
143
|
+
rmSync(target, { recursive: true, force: true })
|
|
144
|
+
return true
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Open VSX 单扩展元数据(latest + 下载 URL)。 */
|
|
148
|
+
export async function marketGet(namespace: string, name: string, version?: string): Promise<Record<string, unknown> | null> {
|
|
149
|
+
const path = version ? namespace + '/' + name + '/' + version : namespace + '/' + name
|
|
150
|
+
const res = await fetch(OPEN_VSX + '/api/' + path, { headers: { accept: 'application/json' } })
|
|
151
|
+
if (!res.ok) return null
|
|
152
|
+
return (await res.json()) as Record<string, unknown>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Open VSX 搜索。 */
|
|
156
|
+
export async function marketSearch(query: string, size = 12): Promise<MarketItem[]> {
|
|
157
|
+
const url = OPEN_VSX + '/api/-/search?query=' + encodeURIComponent(query) + '&size=' + size
|
|
158
|
+
const res = await fetch(url, { headers: { accept: 'application/json' } })
|
|
159
|
+
if (!res.ok) throw new Error('市场搜索失败:HTTP ' + res.status)
|
|
160
|
+
const data = (await res.json()) as { extensions?: unknown[] }
|
|
161
|
+
return ((data.extensions ?? []) as Record<string, unknown>[]).map((item) => ({
|
|
162
|
+
namespace: String(item.namespace ?? ''),
|
|
163
|
+
name: String(item.name ?? ''),
|
|
164
|
+
id: String(item.namespace ?? '') + '.' + String(item.name ?? ''),
|
|
165
|
+
version: String(item.version ?? ''),
|
|
166
|
+
displayName: typeof item.displayName === 'string' ? item.displayName : undefined,
|
|
167
|
+
description: typeof item.description === 'string' ? item.description : undefined,
|
|
168
|
+
download: typeof (item.files as { download?: unknown } | undefined)?.download === 'string' ? (item.files as { download: string }).download : undefined,
|
|
169
|
+
}))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 平台匹配的 VSIX 下载 URL(探测平台变体,失败回退 API 默认)。 */
|
|
173
|
+
export async function resolveDownload(namespace: string, name: string, version: string): Promise<string> {
|
|
174
|
+
const candidate = OPEN_VSX + '/api/' + namespace + '/' + name + '/' + PLATFORM + '/' + version +
|
|
175
|
+
'/file/' + namespace + '.' + name + '-' + version + '@' + PLATFORM + '.vsix'
|
|
176
|
+
try {
|
|
177
|
+
const head = await fetch(candidate, { method: 'HEAD' })
|
|
178
|
+
if (head.ok) return candidate
|
|
179
|
+
} catch (error) {
|
|
180
|
+
/* 探测失败走默认 */
|
|
181
|
+
}
|
|
182
|
+
const meta = await marketGet(namespace, name, version)
|
|
183
|
+
const download = (meta?.files as { download?: unknown } | undefined)?.download
|
|
184
|
+
if (typeof download === 'string') return download
|
|
185
|
+
throw new Error('市场未提供 ' + namespace + '.' + name + '@' + version + ' 的下载地址')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 从 Open VSX 安装/更新扩展。 */
|
|
189
|
+
export async function installFromMarket(namespace: string, name: string, version?: string): Promise<ExtInfo> {
|
|
190
|
+
const meta = await marketGet(namespace, name, version)
|
|
191
|
+
if (!meta) throw new Error('市场找不到 ' + namespace + '.' + name + (version ? '@' + version : ''))
|
|
192
|
+
const ver = version ?? String(meta.version ?? '')
|
|
193
|
+
const url = await resolveDownload(namespace, name, ver)
|
|
194
|
+
const res = await fetch(url)
|
|
195
|
+
if (!res.ok) throw new Error('下载失败:HTTP ' + res.status)
|
|
196
|
+
const vsix = Buffer.from(await res.arrayBuffer())
|
|
197
|
+
return installVsixBuffer(vsix)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 更新已装扩展(对比市场最新版;返回 null 表示已最新)。 */
|
|
201
|
+
export async function updateExtension(id: string): Promise<ExtInfo | null> {
|
|
202
|
+
const installed = listInstalled().find((e) => e.id === id)
|
|
203
|
+
if (!installed) throw new Error('扩展未安装:' + id)
|
|
204
|
+
const meta = await marketGet(installed.namespace, installed.name)
|
|
205
|
+
const latest = String(meta?.version ?? '')
|
|
206
|
+
if (!latest || latest === installed.version) return null
|
|
207
|
+
return installFromMarket(installed.namespace, installed.name, latest)
|
|
208
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — LSP JSON-RPC 帧编解码(纯函数,node 可测)。
|
|
3
|
+
* LSP over stdio 用 Content-Length 帧:
|
|
4
|
+
* Content-Length: <n>\r\n\r\n<恰好 n 字节 JSON>
|
|
5
|
+
* 导出:encodeMessage(对象→帧 Buffer)、createFrameParser(流式粘包/拆包)、parseFrame(单帧)。
|
|
6
|
+
* 作者 ddj 2026-08-27
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const HEADER_END = '\r\n\r\n'
|
|
10
|
+
|
|
11
|
+
/** 消息头正则(仅取 Content-Length;其它头忽略)。 */
|
|
12
|
+
const HEADER_RE = /^Content-Length:\s*(\d+)\s*$/im
|
|
13
|
+
|
|
14
|
+
/** 将对象编码为带 Content-Length 头的帧。 */
|
|
15
|
+
export function encodeMessage(message: unknown): Buffer {
|
|
16
|
+
const body = JSON.stringify(message)
|
|
17
|
+
const head = 'Content-Length: ' + Buffer.byteLength(body, 'utf8') + '\r\n\r\n'
|
|
18
|
+
return Buffer.from(head + body, 'utf8')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 尝试从 buffer 解析单个帧;成功返回 { body, rest },数据不足返回 null。
|
|
23
|
+
* @author ddj 2026年08月27号
|
|
24
|
+
* @param buffer 已累积的字节
|
|
25
|
+
* @returns 解析结果(rest 为剩余未消费字节)
|
|
26
|
+
*/
|
|
27
|
+
export function parseFrame(buffer: Buffer): { body: string; rest: Buffer } | null {
|
|
28
|
+
const headEnd = buffer.indexOf(HEADER_END)
|
|
29
|
+
if (headEnd < 0) return null
|
|
30
|
+
const header = buffer.subarray(0, headEnd).toString('utf8')
|
|
31
|
+
const m = HEADER_RE.exec(header)
|
|
32
|
+
if (!m) {
|
|
33
|
+
// 非法头:丢弃直到下一个帧头,避免死循环
|
|
34
|
+
const next = buffer.indexOf('Content-Length:')
|
|
35
|
+
return next >= 0 ? parseFrame(buffer.subarray(next)) : null
|
|
36
|
+
}
|
|
37
|
+
const length = Number(m[1])
|
|
38
|
+
const bodyStart = headEnd + HEADER_END.length
|
|
39
|
+
if (buffer.length < bodyStart + length) return null
|
|
40
|
+
const body = buffer.subarray(bodyStart, bodyStart + length).toString('utf8')
|
|
41
|
+
return { body, rest: buffer.subarray(bodyStart + length) }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 帧解析器:push 数据块,吐出完整消息(可能拆包后重组、也可能一包多帧)。 */
|
|
45
|
+
export function createFrameParser(): {
|
|
46
|
+
push(chunk: Buffer): unknown[]
|
|
47
|
+
clear(): void
|
|
48
|
+
} {
|
|
49
|
+
let buffer: Buffer = Buffer.alloc(0)
|
|
50
|
+
return {
|
|
51
|
+
push(chunk: Buffer): unknown[] {
|
|
52
|
+
if (chunk.length) buffer = Buffer.concat([buffer, chunk]) as Buffer
|
|
53
|
+
const out: unknown[] = []
|
|
54
|
+
for (;;) {
|
|
55
|
+
const parsed = parseFrame(buffer)
|
|
56
|
+
if (!parsed) break
|
|
57
|
+
buffer = parsed.rest
|
|
58
|
+
try {
|
|
59
|
+
out.push(JSON.parse(parsed.body))
|
|
60
|
+
} catch (error) {
|
|
61
|
+
out.push({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error: ' + String(error) }, id: null })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return out
|
|
65
|
+
},
|
|
66
|
+
clear(): void {
|
|
67
|
+
buffer = Buffer.alloc(0)
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 构造标准 JSON-RPC 请求消息。 */
|
|
73
|
+
export function requestMessage(id: number, method: string, params: unknown): Record<string, unknown> {
|
|
74
|
+
return { jsonrpc: '2.0', id, method, params }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 构造标准 JSON-RPC 通知消息(无 id,fire-and-forget)。 */
|
|
78
|
+
export function notifyMessage(method: string, params: unknown): Record<string, unknown> {
|
|
79
|
+
return { jsonrpc: '2.0', method, params }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 构造标准 JSON-RPC 响应消息。 */
|
|
83
|
+
export function responseMessage(id: number | string | null, result: unknown, error?: unknown): Record<string, unknown> {
|
|
84
|
+
if (error !== undefined) return { jsonrpc: '2.0', id, error }
|
|
85
|
+
return { jsonrpc: '2.0', id, result }
|
|
86
|
+
}
|