dsh-vscode-mode 0.1.32 → 0.1.36
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 +1229 -124
- 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/paths.ts +2 -0
- package/src/client/state/workspaceFoldCache.ts +95 -0
- package/src/client/styles/editor.css +15 -0
- package/src/client/styles/mcp.css +50 -12
- package/src/client/ui/EditorView.ts +55 -1
- package/src/client/ui/KeybindingsPanel.ts +8 -5
- package/src/client/ui/LspSettings.ts +274 -0
- package/src/client/ui/McpSettings.ts +112 -49
- package/src/client/ui/PerfSettings.ts +145 -34
- 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
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import React from 'react'
|
|
12
12
|
import { rpc } from '../rpc.js'
|
|
13
|
+
import { foldLoad, foldSave, normalizeFoldKey } from '../state/workspaceFoldCache.js'
|
|
13
14
|
import '../styles/mcp.css'
|
|
14
15
|
|
|
15
16
|
/** 字节数人类可读。 */
|
|
@@ -23,6 +24,39 @@ function formatBytes(n) {
|
|
|
23
24
|
/** 会话行 key:workspaceKey \u0000 sessionId。 */
|
|
24
25
|
const keyOf = (ws, id) => ws + '\u0000' + id
|
|
25
26
|
|
|
27
|
+
/** 工作区列表每页条数。 */
|
|
28
|
+
const PAGE_SIZE = 7
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 计算分页(纯函数,可单测)。
|
|
32
|
+
* 作者 ddj 2026年09月02号
|
|
33
|
+
* @param total 总条数
|
|
34
|
+
* @param pageSize 每页条数
|
|
35
|
+
* @param page 当前页码(1 起)
|
|
36
|
+
* @returns 归一化后的分页信息(page 被夹到有效范围)
|
|
37
|
+
*/
|
|
38
|
+
export function paginate(total, pageSize, page) {
|
|
39
|
+
const size = Math.max(1, Math.floor(pageSize) || 1)
|
|
40
|
+
const pages = Math.max(1, Math.ceil(total / size))
|
|
41
|
+
const current = Math.min(Math.max(1, Math.floor(page) || 1), pages)
|
|
42
|
+
const start = (current - 1) * size
|
|
43
|
+
return { page: current, pages, pageSize: size, start, end: Math.min(start + size, total), total }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 取当前页切片(纯函数,可单测)。
|
|
48
|
+
* 作者 ddj 2026年09月02号
|
|
49
|
+
* @param items 全量列表
|
|
50
|
+
* @param page 当前页码(1 起)
|
|
51
|
+
* @param pageSize 每页条数
|
|
52
|
+
* @returns 当前页子数组
|
|
53
|
+
*/
|
|
54
|
+
export function pageSlice(items, page, pageSize) {
|
|
55
|
+
const list = items ?? []
|
|
56
|
+
const { start, end } = paginate(list.length, pageSize, page)
|
|
57
|
+
return list.slice(start, end)
|
|
58
|
+
}
|
|
59
|
+
|
|
26
60
|
/** 单行会话。 */
|
|
27
61
|
function SessionRow({ row, checked, disabled, onToggle }) {
|
|
28
62
|
const age = Math.max(0, (Date.now() - row.mtime) / (24 * 3600 * 1000))
|
|
@@ -36,6 +70,56 @@ function SessionRow({ row, checked, disabled, onToggle }) {
|
|
|
36
70
|
)
|
|
37
71
|
}
|
|
38
72
|
|
|
73
|
+
/**
|
|
74
|
+
* 工作区列表分页器:顶部文案 + 上一页/下一页交互。
|
|
75
|
+
* 作者 ddj 2026年09月02号
|
|
76
|
+
* @param {object} info paginate() 的分页信息
|
|
77
|
+
* @param {Function} onPrev 上一页回调
|
|
78
|
+
* @param {Function} onNext 下一页回调
|
|
79
|
+
*/
|
|
80
|
+
function WorkspacePager({ info, onPrev, onNext }) {
|
|
81
|
+
const { page, pages, start, end, total } = info
|
|
82
|
+
return React.createElement('div', { className: 'vsm-perf-pager' },
|
|
83
|
+
React.createElement('span', { className: 'vsm-perf-pager-text' },
|
|
84
|
+
total ? `第 ${start + 1}–${end} 个 / 共 ${total} 个工作区(第 ${page}/${pages} 页)` : '暂无工作区',
|
|
85
|
+
),
|
|
86
|
+
React.createElement('div', { className: 'vsm-perf-pager-actions' },
|
|
87
|
+
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: onPrev, disabled: page <= 1 }, '‹ 上一页'),
|
|
88
|
+
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: onNext, disabled: page >= pages }, '下一页 ›'),
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 工作区栏目(可展开/折叠),内含该工作区的会话行。
|
|
95
|
+
* 作者 ddj 2026年09月02号
|
|
96
|
+
* @param {object} ws 工作区汇总(workspaceKey / totalBytes)
|
|
97
|
+
* @param {object[]} rows 该工作区的会话行
|
|
98
|
+
* @param {boolean} expanded 是否展开
|
|
99
|
+
* @param {Function} onToggleFold 展开/折叠切换回调
|
|
100
|
+
* @param {object} selected 已选会话键集合
|
|
101
|
+
* @param {boolean} locked 会话勾选是否禁用(移出执行中)
|
|
102
|
+
* @param {Function} onToggleRow 单行勾选回调
|
|
103
|
+
*/
|
|
104
|
+
function WorkspaceBlock({ ws, rows, expanded, onToggleFold, selected, locked, onToggleRow }) {
|
|
105
|
+
const head = React.createElement('div', { className: 'vsm-perf-ws-head', role: 'button', tabIndex: 0, 'aria-expanded': expanded, onClick: onToggleFold, onKeyDown: (event) => {
|
|
106
|
+
if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); onToggleFold() }
|
|
107
|
+
} },
|
|
108
|
+
React.createElement('span', { className: 'vsm-perf-ws-caret ' + (expanded ? 'open' : '') }, '▸'),
|
|
109
|
+
React.createElement('span', { className: 'vsm-perf-ws-name', title: ws.workspaceKey }, ws.workspaceKey),
|
|
110
|
+
React.createElement('span', { className: 'vsm-perf-ws-meta' }, rows.length + ' 会话 · ' + formatBytes(ws.totalBytes)),
|
|
111
|
+
)
|
|
112
|
+
if (!expanded) return React.createElement('div', { className: 'vsm-perf-ws' }, head)
|
|
113
|
+
const body = rows.length ? rows.map((row) => React.createElement(SessionRow, {
|
|
114
|
+
key: keyOf(row.workspaceKey, row.sessionId),
|
|
115
|
+
row,
|
|
116
|
+
checked: !!selected[keyOf(row.workspaceKey, row.sessionId)],
|
|
117
|
+
disabled: row.active || locked,
|
|
118
|
+
onToggle: () => onToggleRow(keyOf(row.workspaceKey, row.sessionId)),
|
|
119
|
+
})) : React.createElement('div', { className: 'vsm-mcp-empty' }, '无会话')
|
|
120
|
+
return React.createElement('div', { className: 'vsm-perf-ws expanded' }, head, React.createElement('div', { className: 'vsm-perf-rows' }, body))
|
|
121
|
+
}
|
|
122
|
+
|
|
39
123
|
/**
|
|
40
124
|
* 会话卫生 + 压缩调优设置子页。
|
|
41
125
|
* @author ddj 2026年09月02号
|
|
@@ -48,6 +132,23 @@ export function PerfSettings() {
|
|
|
48
132
|
const [confirm, setConfirm] = React.useState(null)
|
|
49
133
|
const [moved, setMoved] = React.useState(null)
|
|
50
134
|
const [config, setConfig] = React.useState(null)
|
|
135
|
+
const [expanded, setExpanded] = React.useState(() => foldLoad())
|
|
136
|
+
const [page, setPage] = React.useState(1)
|
|
137
|
+
|
|
138
|
+
React.useEffect(() => { foldSave(expanded) }, [expanded])
|
|
139
|
+
|
|
140
|
+
/** 数据变化后把越界页码夹回有效范围。 */
|
|
141
|
+
const workspaces = inv?.workspaces ?? []
|
|
142
|
+
const pageInfo = paginate(workspaces.length, PAGE_SIZE, page)
|
|
143
|
+
React.useEffect(() => {
|
|
144
|
+
if (page !== pageInfo.page) setPage(pageInfo.page)
|
|
145
|
+
}, [page, pageInfo.page])
|
|
146
|
+
|
|
147
|
+
/** 切换某工作区栏目的展开/折叠(默认全折叠)。 */
|
|
148
|
+
const toggleFold = (key) => setExpanded((old) => old.includes(key) ? old.filter((item) => item !== key) : old.concat(key))
|
|
149
|
+
|
|
150
|
+
/** 切换单会话勾选。 */
|
|
151
|
+
const toggleRow = (k) => setSelected((old) => { const next = { ...old }; if (next[k]) delete next[k]; else next[k] = true; return next })
|
|
51
152
|
|
|
52
153
|
const load = React.useCallback(() => {
|
|
53
154
|
setError('')
|
|
@@ -156,27 +257,25 @@ export function PerfSettings() {
|
|
|
156
257
|
React.createElement('div', { className: 'vsm-perf-card' }, React.createElement('span', null, '压缩态总体积'), React.createElement('b', { className: totals.totalBytes >= 50 * 1024 * 1024 ? 'big' : '' }, formatBytes(totals.totalBytes))),
|
|
157
258
|
) : null
|
|
158
259
|
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
React.createElement('span', { className: 'vsm-perf-ws-meta' }, rows.length + ' 会话 · ' + formatBytes(ws.totalBytes)),
|
|
165
|
-
),
|
|
166
|
-
React.createElement('div', { className: 'vsm-perf-rows' },
|
|
167
|
-
rows.length ? rows.map((row) => React.createElement(SessionRow, {
|
|
168
|
-
key: keyOf(row.workspaceKey, row.sessionId),
|
|
169
|
-
row,
|
|
170
|
-
checked: !!selected[keyOf(row.workspaceKey, row.sessionId)],
|
|
171
|
-
disabled: row.active || busy === 'move',
|
|
172
|
-
onToggle: () => {
|
|
173
|
-
const k = keyOf(row.workspaceKey, row.sessionId)
|
|
174
|
-
setSelected((old) => { const next = { ...old }; if (next[k]) delete next[k]; else next[k] = true; return next })
|
|
175
|
-
},
|
|
176
|
-
})) : React.createElement('div', { className: 'vsm-mcp-empty' }, '无会话'),
|
|
177
|
-
),
|
|
178
|
-
)
|
|
260
|
+
const visible = pageSlice(workspaces, pageInfo.page, PAGE_SIZE)
|
|
261
|
+
const workspacePager = workspaces.length > PAGE_SIZE ? React.createElement(WorkspacePager, {
|
|
262
|
+
info: pageInfo,
|
|
263
|
+
onPrev: () => setPage((old) => Math.max(1, old - 1)),
|
|
264
|
+
onNext: () => setPage((old) => Math.min(pageInfo.pages, old + 1)),
|
|
179
265
|
}) : null
|
|
266
|
+
const workspaceBlocks = workspaces.length ? React.createElement('div', { className: 'vsm-perf-ws-list' }, visible.map((ws) => {
|
|
267
|
+
const key = normalizeFoldKey(ws.workspaceKey)
|
|
268
|
+
return React.createElement(WorkspaceBlock, {
|
|
269
|
+
key: ws.workspaceKey,
|
|
270
|
+
ws,
|
|
271
|
+
rows: inv.sessions.filter((s) => s.workspaceKey === ws.workspaceKey),
|
|
272
|
+
expanded: expanded.includes(key),
|
|
273
|
+
onToggleFold: () => toggleFold(key),
|
|
274
|
+
selected,
|
|
275
|
+
locked: busy === 'move',
|
|
276
|
+
onToggleRow: toggleRow,
|
|
277
|
+
})
|
|
278
|
+
})) : null
|
|
180
279
|
|
|
181
280
|
const selCount = Object.keys(selected).length
|
|
182
281
|
const confirmBody = confirm ? React.createElement('div', { className: 'vsm-perf-modal' },
|
|
@@ -192,18 +291,21 @@ export function PerfSettings() {
|
|
|
192
291
|
),
|
|
193
292
|
) : null
|
|
194
293
|
|
|
195
|
-
const movedBody = moved?.moved?.length ? React.createElement('
|
|
196
|
-
React.createElement('h3',
|
|
294
|
+
const movedBody = moved?.moved?.length ? React.createElement('section', { className: 'vsm-panel' },
|
|
295
|
+
React.createElement('h3', { className: 'vsm-panel-title' }, '已移出 ' + moved.moved.length + ' 个会话(释放 ' + formatBytes(moved.reclaimedBytes) + ')'),
|
|
296
|
+
React.createElement('div', { className: 'vsm-panel-body' },
|
|
197
297
|
React.createElement('div', { className: 'vsm-perf-rows' }, moved.moved.map((item) => React.createElement('div', { key: item.workspaceKey + item.sessionId, className: 'vsm-perf-row' },
|
|
198
298
|
React.createElement('span', { className: 'vsm-perf-cell vsm-perf-id' }, item.sessionId),
|
|
199
299
|
React.createElement('span', { className: 'vsm-perf-cell vsm-perf-size' }, formatBytes(item.bytes)),
|
|
200
300
|
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: () => doRestore(item), disabled: busy === 'restore' }, '恢复'),
|
|
201
301
|
))),
|
|
202
302
|
moved.failures?.length ? React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, '失败:' + moved.failures.join(';')) : null,
|
|
303
|
+
),
|
|
203
304
|
) : null
|
|
204
305
|
|
|
205
|
-
const configBody = config ? React.createElement('
|
|
206
|
-
React.createElement('h3',
|
|
306
|
+
const configBody = config ? React.createElement('section', { className: 'vsm-panel' },
|
|
307
|
+
React.createElement('h3', { className: 'vsm-panel-title' }, 'DSH 内置压缩调优'),
|
|
308
|
+
React.createElement('div', { className: 'vsm-panel-body' },
|
|
207
309
|
React.createElement('p', null, '调低 compaction-basic / tool-result-pruner 触发阈值,让长循环会话更早压缩,降低模型可见上下文压力(减少 token 与防 context overflow)。'),
|
|
208
310
|
React.createElement('p', { className: 'vsm-perf-note' }, '注意:压缩只追加 compaction 事件,不重写已持久化会话日志(append-only),因此不会缩小已存在的会话文件——存量瘦身请用上方「移出到归档」。packChunks 已默认开启,无需配置。'),
|
|
209
311
|
React.createElement('div', { className: 'vsm-perf-config-state' },
|
|
@@ -211,13 +313,16 @@ export function PerfSettings() {
|
|
|
211
313
|
config.applied ? React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: undoConfig, disabled: busy === 'undo' }, '撤销') : React.createElement('button', { className: 'vsm-primary vsm-small', onClick: applyConfig, disabled: busy === 'apply' }, '应用建议配置(需重启)'),
|
|
212
314
|
),
|
|
213
315
|
React.createElement('pre', { className: 'vsm-perf-block' }, config.block),
|
|
316
|
+
),
|
|
214
317
|
) : null
|
|
215
318
|
|
|
216
|
-
const guideBody = React.createElement('
|
|
217
|
-
React.createElement('h3',
|
|
319
|
+
const guideBody = React.createElement('section', { className: 'vsm-panel' },
|
|
320
|
+
React.createElement('h3', { className: 'vsm-panel-title' }, '侧车摘要(agent/脚本取关键字段,不整份读 sidecar)'),
|
|
321
|
+
React.createElement('div', { className: 'vsm-panel-body' },
|
|
218
322
|
React.createElement('p', null, '工作区根目录的 .dsh-edit-review.json(可能数 MB)只应通过摘要接口读取,禁止 agent 整份 read/write 进对话:'),
|
|
219
323
|
React.createElement('pre', { className: 'vsm-perf-block' },
|
|
220
324
|
"curl -s -X POST http://127.0.0.1:3080/edrv/rpc -H 'content-type: application/json' -d '{\"method\":\"edrv.perf.sidecarSummary\",\"args\":{}}'\n# 差异记录列表(含每文件 pending 摘要):edrv.list\n# 历史归档:edrv.archiveList / edrv.archiveRead"),
|
|
325
|
+
),
|
|
221
326
|
)
|
|
222
327
|
|
|
223
328
|
return React.createElement('section', { className: 'vsm-perf-page' },
|
|
@@ -225,15 +330,21 @@ export function PerfSettings() {
|
|
|
225
330
|
React.createElement('p', null, 'DSH 启动会回放 ~/.dsh/sessions 全部会话(V8 展开约 10×),体积越大内存越吃紧。定期把巨型/旧会话移出到归档可显著降低启动内存。'),
|
|
226
331
|
error ? React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error) : null,
|
|
227
332
|
summaryCards,
|
|
228
|
-
React.createElement('
|
|
229
|
-
React.createElement('
|
|
230
|
-
React.createElement('
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
333
|
+
React.createElement('section', { className: 'vsm-panel' },
|
|
334
|
+
React.createElement('h3', { className: 'vsm-panel-title' }, '会话盘点'),
|
|
335
|
+
React.createElement('div', { className: 'vsm-panel-body' },
|
|
336
|
+
React.createElement('div', { className: 'vsm-perf-toolbar' },
|
|
337
|
+
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: selectBig }, '圈选 ≥1MB 非活跃'),
|
|
338
|
+
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: clearSel, disabled: !selCount }, '清空选择'),
|
|
339
|
+
React.createElement('span', { className: 'vsm-perf-selcount' }, '已选 ' + selCount + ' 个'),
|
|
340
|
+
React.createElement('button', { className: 'vsm-primary vsm-small', onClick: planMove, disabled: !selCount || busy === 'plan' }, busy === 'plan' ? '规划中…' : '移出所选'),
|
|
341
|
+
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: doPurge, disabled: busy === 'purge' }, '清除归档(>30天)'),
|
|
342
|
+
React.createElement('button', { className: 'vsm-secondary vsm-small', onClick: load, disabled: busy === 'load' }, '⟳ 刷新'),
|
|
343
|
+
),
|
|
344
|
+
workspacePager,
|
|
345
|
+
workspaceBlocks,
|
|
346
|
+
),
|
|
235
347
|
),
|
|
236
|
-
workspaceBlocks,
|
|
237
348
|
movedBody,
|
|
238
349
|
configBody,
|
|
239
350
|
guideBody,
|
package/src/fileOpenSettings.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface SettingsDeps {
|
|
|
24
24
|
z: {
|
|
25
25
|
object: (shape: Record<string, unknown>) => { default: (value: Record<string, unknown>) => unknown }
|
|
26
26
|
string: () => { default: (value: string) => unknown }
|
|
27
|
+
boolean: () => { default: (value: boolean) => unknown }
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,11 @@ import { setupOpenSettings } from './fileOpenSettings.js'
|
|
|
18
18
|
import { disposeIndex } from './treeIndex.js'
|
|
19
19
|
import { sweepTreeCache } from './paths.js'
|
|
20
20
|
import { PLUGIN_NAME, buildReport } from './compat.js'
|
|
21
|
+
import { createLspManager } from './lsp/manager.js'
|
|
22
|
+
import { createLspRpc } from './lsp/rpc.js'
|
|
23
|
+
import { installLspSettingsSection } from './lsp/settings.js'
|
|
24
|
+
import { disposeAllServers } from './lsp/transport.js'
|
|
25
|
+
import type { RpcHandlerMap } from './shared/rpc.js'
|
|
21
26
|
import type { Registry } from './registry.js'
|
|
22
27
|
import type { Ctx } from './store.js'
|
|
23
28
|
|
|
@@ -34,9 +39,15 @@ export function apply(ctx: Ctx, config?: unknown): void {
|
|
|
34
39
|
const registry: Registry = new Map()
|
|
35
40
|
const searcher = newSearcher(ctx)
|
|
36
41
|
const contentSearcher = newContentSearcher(ctx)
|
|
42
|
+
/** LSP 服务器管理器(语言智能:跳转/引用/大纲)。 */
|
|
43
|
+
const lspManager = createLspManager((line) => ctx.logger?.debug?.('[' + name + '] ' + line))
|
|
37
44
|
/** 兼容性警告收集(route 护栏等写入,启动日志一并输出)。 */
|
|
38
45
|
const warnings: string[] = []
|
|
39
46
|
setupOpenSettings(ctx, config, () => {})
|
|
47
|
+
void installLspSettingsSection(ctx, {})
|
|
48
|
+
/** LSP RPC 与会话清理(一次性创建,tracker 状态跨请求保留)。 */
|
|
49
|
+
const lspRpc = createLspRpc({ ctx, pluginConfig: config, manager: lspManager })
|
|
50
|
+
const lspHandlers: Partial<RpcHandlerMap> = lspRpc.handlers
|
|
40
51
|
|
|
41
52
|
ctx.on('tools/result', (exec: unknown, result: unknown) => {
|
|
42
53
|
void captureToolResult(ctx, registry, exec, result)
|
|
@@ -51,15 +62,22 @@ export function apply(ctx: Ctx, config?: unknown): void {
|
|
|
51
62
|
contentSearcher.dispose(cwd)
|
|
52
63
|
disposeIndex(cwd)
|
|
53
64
|
}
|
|
65
|
+
const sid = (session as { id?: unknown })?.id
|
|
66
|
+
if (typeof sid === 'string') lspRpc.disposeSession(sid)
|
|
54
67
|
})
|
|
55
68
|
|
|
56
|
-
registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher), (warning) => warnings.push(warning))
|
|
69
|
+
registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspHandlers), (warning) => warnings.push(warning))
|
|
57
70
|
installIsolation(ctx)
|
|
58
71
|
// 启动清理缓存目录:非当前 schema / 超保留期 / 未知残留(best-effort,不阻塞装配)
|
|
59
72
|
void sweepTreeCache()
|
|
60
73
|
void logCompatSummary(ctx, warnings)
|
|
74
|
+
// 卸载/重启时强杀 LSP 子进程(防残留)
|
|
75
|
+
ctx.effect(() => () => {
|
|
76
|
+
void lspManager.disposeAll().catch(() => {})
|
|
77
|
+
disposeAllServers()
|
|
78
|
+
})
|
|
61
79
|
|
|
62
|
-
ctx.logger?.info?.('[' + name + '] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP
|
|
80
|
+
ctx.logger?.info?.('[' + name + '] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用,语言服务器 LSP 已接入)')
|
|
63
81
|
}
|
|
64
82
|
|
|
65
83
|
/**
|
|
@@ -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
|
+
}
|