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
|
@@ -129,6 +129,14 @@
|
|
|
129
129
|
.vsm-kb-dirty{flex:1;font-size:12px;color:var(--dsw-alias-state-warn-primary,#e5b93f)}
|
|
130
130
|
.vsm-kb-message{font-size:12px;color:var(--dsw-alias-brand-primary,#4f8cff);margin-top:8px}
|
|
131
131
|
|
|
132
|
+
/* 语言服务器(LSP)设置子 Tab */
|
|
133
|
+
.vsm-lsp-form{display:flex;flex-direction:column;gap:6px;margin-top:8px}
|
|
134
|
+
.vsm-lsp-form label{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--dsw-alias-label-secondary,#9aa5b1)}
|
|
135
|
+
.vsm-lsp-form input{height:26px;padding:0 8px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:var(--dsw-alias-bg-base,#111);color:var(--dsw-alias-label-primary,#eee);font-size:12px;outline:none}
|
|
136
|
+
.vsm-lsp-form input:focus{border-color:var(--dsw-alias-brand-primary,#4f8cff)}
|
|
137
|
+
.vsm-lsp-form button{align-self:flex-start;margin-top:2px}
|
|
138
|
+
.vsm-lsp-hint{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);margin-top:10px;line-height:1.6}
|
|
139
|
+
|
|
132
140
|
/* 会话性能(PerfSettings 子 Tab) */
|
|
133
141
|
.vsm-perf-page{display:flex;flex-direction:column;gap:14px;max-width:920px}
|
|
134
142
|
.vsm-perf-page h2{margin:0;font-size:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
@@ -176,3 +184,10 @@
|
|
|
176
184
|
.vsm-secondary:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
|
|
177
185
|
.vsm-secondary:disabled{opacity:.55;cursor:default}
|
|
178
186
|
.vsm-small{padding:4px 10px;font-size:12px}
|
|
187
|
+
|
|
188
|
+
/* 扩展管理(LSP 语言服务器) */
|
|
189
|
+
.vsm-mcp-actions button.vsm-primary,
|
|
190
|
+
.vsm-mcp-actions button.vsm-danger{width:auto;height:auto;padding:5px 12px;font-size:12px;border-radius:6px;line-height:1.4}
|
|
191
|
+
.vsm-mcp-actions button.vsm-danger{color:var(--dsw-alias-state-error-primary,#f07878);background:none}
|
|
192
|
+
.vsm-mcp-actions button.vsm-danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger,#ff000014)}
|
|
193
|
+
.vsm-ext-ver{font-weight:400;font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -23,6 +23,8 @@ import { setSidePending, SIDEBAR_INSTALL_CMD } from '../sidebarBridge.js'
|
|
|
23
23
|
import { upsertViewState, viewStatesLoad, viewStatesSave } from '../state/viewStateCache.js'
|
|
24
24
|
import { bindingOf, chordOf, matchEvent, useKeybindingsVersion } from '../keybindings.js'
|
|
25
25
|
import { CACHE_KEY } from '../paths.js'
|
|
26
|
+
import { bindLspEditor, runGoToDefinition, runFindReferences, hideReferencesOverlay } from '../monaco/lsp/providers.js'
|
|
27
|
+
import { onLspProgress, refreshStatus } from '../monaco/lsp/index.js'
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* 中央编辑区:文件页签(脏点/关闭/打开路径)+ Ctrl+P 搜索 + Monaco 编辑器 +
|
|
@@ -47,6 +49,7 @@ export function EditorView(props) {
|
|
|
47
49
|
const [content, setContent] = React.useState(null)
|
|
48
50
|
const [contentPath, setContentPath] = React.useState(null)
|
|
49
51
|
const [status, setStatus] = React.useState('')
|
|
52
|
+
const [lspServers, setLspServers] = React.useState([])
|
|
50
53
|
const [error, setError] = React.useState(null)
|
|
51
54
|
const [loadError, setLoadError] = React.useState(null)
|
|
52
55
|
const [loadStage, setLoadStage] = React.useState({ progress: 0, message: '准备加载编辑器…' })
|
|
@@ -265,6 +268,14 @@ export function EditorView(props) {
|
|
|
265
268
|
return () => { clearInterval(t); window.removeEventListener('edrv:refresh', onRefresh) }
|
|
266
269
|
}, [sessionId])
|
|
267
270
|
|
|
271
|
+
React.useEffect(() => {
|
|
272
|
+
const publishLsp = (servers) => setLspServers(Array.isArray(servers) ? servers : [])
|
|
273
|
+
const unsubscribe = onLspProgress(publishLsp)
|
|
274
|
+
void refreshStatus(true).then(publishLsp)
|
|
275
|
+
const timer = setInterval(() => { void refreshStatus(true).then(publishLsp) }, 1500)
|
|
276
|
+
return () => { unsubscribe(); clearInterval(timer) }
|
|
277
|
+
}, [sessionId])
|
|
278
|
+
|
|
268
279
|
React.useEffect(() => {
|
|
269
280
|
const onOpen = (e) => {
|
|
270
281
|
const p = e?.detail?.path
|
|
@@ -272,6 +283,13 @@ export function EditorView(props) {
|
|
|
272
283
|
if (e?.detail?.focusDiff === true) {
|
|
273
284
|
pendingFocusRef.current = { path: p, region: null }
|
|
274
285
|
setFocusRequest((value) => value + 1)
|
|
286
|
+
addTab(p, true)
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
if (e?.detail?.line != null) {
|
|
290
|
+
// LSP/搜索跳转:打开并定位到行列
|
|
291
|
+
openFileAt(p, e?.detail?.line, e?.detail?.column)
|
|
292
|
+
return
|
|
275
293
|
}
|
|
276
294
|
addTab(p, true)
|
|
277
295
|
}
|
|
@@ -282,9 +300,12 @@ export function EditorView(props) {
|
|
|
282
300
|
}
|
|
283
301
|
window.addEventListener('edrv:open-editor', onOpen)
|
|
284
302
|
window.addEventListener('edrv:show-launcher', onShowLauncher)
|
|
303
|
+
const onStatus = (e) => { if (e?.detail?.text) setStatus(e.detail.text) }
|
|
304
|
+
window.addEventListener('edrv:status', onStatus)
|
|
285
305
|
return () => {
|
|
286
306
|
window.removeEventListener('edrv:open-editor', onOpen)
|
|
287
307
|
window.removeEventListener('edrv:show-launcher', onShowLauncher)
|
|
308
|
+
window.removeEventListener('edrv:status', onStatus)
|
|
288
309
|
}
|
|
289
310
|
}, [sessionId])
|
|
290
311
|
|
|
@@ -543,6 +564,7 @@ export function EditorView(props) {
|
|
|
543
564
|
flushSave()
|
|
544
565
|
saveViewState(active)
|
|
545
566
|
diffRendererRef.current?.dispose?.()
|
|
567
|
+
hideReferencesOverlay()
|
|
546
568
|
if (editorRef.current) { editorRef.current.dispose(); editorRef.current = null }
|
|
547
569
|
for (const m of modelsRef.current.values()) m.dispose()
|
|
548
570
|
modelsRef.current.clear()
|
|
@@ -629,6 +651,21 @@ export function EditorView(props) {
|
|
|
629
651
|
precondition: 'editorTextFocus',
|
|
630
652
|
run: (edx) => menuHandlers()?.openInExplorer(pathOf(edx)),
|
|
631
653
|
})
|
|
654
|
+
// 语言智能(LSP):转到定义 / 查找所有引用(右键 + F12/Shift+F12 键位)。
|
|
655
|
+
ed.addAction({
|
|
656
|
+
id: 'edrv.goToDefinition', label: '转到定义', contextMenuGroupId: '1_edrv',
|
|
657
|
+
keybindings: m.KeyMod.CtrlCmd | m.KeyCode.F12,
|
|
658
|
+
precondition: 'editorTextFocus',
|
|
659
|
+
run: (edx) => { void runGoToDefinition(edx) },
|
|
660
|
+
})
|
|
661
|
+
ed.addAction({
|
|
662
|
+
id: 'edrv.findReferences', label: '查找所有引用', contextMenuGroupId: '1_edrv',
|
|
663
|
+
keybindings: m.KeyMod.Shift | m.KeyCode.F12,
|
|
664
|
+
precondition: 'editorTextFocus',
|
|
665
|
+
run: (edx) => { void runFindReferences(edx) },
|
|
666
|
+
})
|
|
667
|
+
ed.addCommand(m.KeyCode.F12, () => { void runGoToDefinition(ed) })
|
|
668
|
+
bindLspEditor(ed)
|
|
632
669
|
// hover 差异块 → 浮出 Keep/Undo(req:鼠标移到编辑区差异块时显示)
|
|
633
670
|
// 防闪烁:① 区域不变不 setState(浮窗锚定差异块起始行,不跟随鼠标);② 延迟隐藏;
|
|
634
671
|
// ③ 浮窗自身 onMouseEnter 取消隐藏计时(鼠标在浮窗与编辑器间移动不闪)。
|
|
@@ -982,6 +1019,22 @@ export function EditorView(props) {
|
|
|
982
1019
|
(cursor ? React.createElement('span', { className: 'edrv-pb-meta' }, cursor) : null),
|
|
983
1020
|
(status ? React.createElement('span', { className: 'edrv-pb-meta edrv-pb-status' }, status) : null))
|
|
984
1021
|
|
|
1022
|
+
const lspLanguage = active ? langOf(active) : ''
|
|
1023
|
+
const lspServer = lspServers.find((item) => item.languageId === lspLanguage)
|
|
1024
|
+
const lspPhaseText = { idle: '未启动', starting: '启动中', ready: '已就绪', indexing: '解析中', unavailable: '不可用', stopped: '已停止' }
|
|
1025
|
+
const lspSourceText = { extension: '扩展', discover: '自动发现', manual: '手动配置', none: '未配置' }
|
|
1026
|
+
const lspLabel = lspServer
|
|
1027
|
+
? 'LSP ' + lspLanguage + ' · ' + (lspPhaseText[lspServer.phase] ?? lspServer.phase) + ' · ' + (lspSourceText[lspServer.source] ?? lspServer.source)
|
|
1028
|
+
: 'LSP ' + lspLanguage + ' · 未启动'
|
|
1029
|
+
const lspProgress = typeof lspServer?.progress === 'number' ? Math.round(lspServer.progress) : null
|
|
1030
|
+
const lspFooter = (lspLanguage === 'lua' || lspLanguage === 'csharp')
|
|
1031
|
+
? React.createElement('div', { className: 'edrv-statusbar', title: lspServer?.progressMessage || lspLabel },
|
|
1032
|
+
React.createElement('span', { className: 'edrv-lsp-status-dot ' + (lspServer?.phase === 'ready' ? 'ready' : lspServer?.phase === 'indexing' || lspServer?.phase === 'starting' ? 'busy' : 'idle') }),
|
|
1033
|
+
React.createElement('span', { className: 'edrv-sp-lsp' }, lspLabel),
|
|
1034
|
+
lspServer?.progressMessage ? React.createElement('span', { className: 'edrv-sp-progress-message' }, lspServer.progressMessage) : null,
|
|
1035
|
+
lspProgress !== null ? React.createElement('span', { className: 'edrv-sp-progress' }, lspProgress + '%') : null)
|
|
1036
|
+
: null
|
|
1037
|
+
|
|
985
1038
|
const tabRow = React.createElement('div', { style: { display: 'flex', alignItems: 'center', background: 'var(--dsw-alias-bg-layer-1,transparent)', flexShrink: 0 } },
|
|
986
1039
|
tabsEl,
|
|
987
1040
|
(sum.totalFiles > 0
|
|
@@ -1169,7 +1222,8 @@ export function EditorView(props) {
|
|
|
1169
1222
|
pathBar,
|
|
1170
1223
|
tabRow,
|
|
1171
1224
|
sideHintEl,
|
|
1172
|
-
editorArea
|
|
1225
|
+
editorArea,
|
|
1226
|
+
lspFooter)
|
|
1173
1227
|
// 编辑器根节点按 composer 顶部边界动态限高,底部对话区域继续由 DSH 原生渲染。
|
|
1174
1228
|
// 侧栏形态由面板容器给高(100%),不做 composer 几何同步。
|
|
1175
1229
|
const editorRow = React.createElement('div', { className: 'edrv-editor-row' },
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — 「语言服务器」设置子 Tab。
|
|
4
|
+
* 四视图:服务器(状态 + 每语言启用/命令/路径)|已安装(卸载/更新)|市场(Open VSX 搜索/安装 + 本地 vsix)|更新。
|
|
5
|
+
* 状态经 edrv.lsp.status,配置经 edrv.lsp.configGet/Update,扩展经 edrv.lsp.ext.*。
|
|
6
|
+
* 作者 ddj 2026-08-27
|
|
7
|
+
*/
|
|
8
|
+
import React from 'react'
|
|
9
|
+
import { rpc } from '../rpc.js'
|
|
10
|
+
import '../styles/mcp.css'
|
|
11
|
+
|
|
12
|
+
const LANGUAGES = [
|
|
13
|
+
{ id: 'lua', label: 'Lua(LuaLS)', hint: 'lua-language-server,EmmyLua 注解原生支持' },
|
|
14
|
+
{ id: 'csharp', label: 'C#(Roslyn / OmniSharp)', hint: '需 dotnet + ms-dotnettools.csharp 的 .roslyn 服务器,或手动指定' },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
const PHASE_LABEL = {
|
|
18
|
+
idle: '未启动', starting: '启动中', ready: '就绪', indexing: '索引中', unavailable: '不可用', stopped: '已停止',
|
|
19
|
+
}
|
|
20
|
+
const SOURCE_LABEL = { extension: '扩展', discover: '自动发现', manual: '手动配置', none: '未配置' }
|
|
21
|
+
const TABS = [
|
|
22
|
+
{ id: 'servers', label: '服务器' },
|
|
23
|
+
{ id: 'installed', label: '已安装' },
|
|
24
|
+
{ id: 'market', label: '市场' },
|
|
25
|
+
{ id: 'updates', label: '更新' },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
/** 语言卡片:状态 + 启用开关 + 命令/路径配置。 */
|
|
29
|
+
function LangCard({ lang, config, status, busy, onToggle, onSave }) {
|
|
30
|
+
const [pathDraft, setPathDraft] = React.useState(config?.path ?? '')
|
|
31
|
+
const [commandDraft, setCommandDraft] = React.useState(config?.command ?? '')
|
|
32
|
+
React.useEffect(() => { setPathDraft(config?.path ?? ''); setCommandDraft(config?.command ?? '') }, [config?.path, config?.command])
|
|
33
|
+
const phase = status?.phase ?? 'idle'
|
|
34
|
+
return React.createElement('article', { className: 'vsm-mcp-card' },
|
|
35
|
+
React.createElement('div', { className: 'vsm-mcp-card-head' },
|
|
36
|
+
React.createElement('div', { className: 'vsm-mcp-title' },
|
|
37
|
+
React.createElement('span', { className: 'vsm-mcp-dot ' + (phase === 'ready' || phase === 'indexing' ? 'connected' : phase === 'unavailable' ? 'error' : 'disabled') }),
|
|
38
|
+
lang.label),
|
|
39
|
+
React.createElement('div', { className: 'vsm-mcp-actions' },
|
|
40
|
+
React.createElement('button', { className: 'vsm-switch ' + (config?.enabled !== false ? 'on' : ''), onClick: () => onToggle(lang.id, config?.enabled !== false ? false : true), 'aria-label': '启用/禁用' }, config?.enabled !== false ? '●' : '○'))),
|
|
41
|
+
React.createElement('div', { className: 'vsm-mcp-meta' },
|
|
42
|
+
PHASE_LABEL[phase] ?? phase, ' · ', SOURCE_LABEL[status?.source] ?? status?.source,
|
|
43
|
+
(phase === 'ready' && status?.root) ? ' · ' + String(status.root).split(/[\\/]/).pop() : ''),
|
|
44
|
+
status?.reason ? React.createElement('div', { className: 'vsm-mcp-error' }, status.reason) : null,
|
|
45
|
+
React.createElement('div', { className: 'vsm-lsp-form' },
|
|
46
|
+
React.createElement('label', null, React.createElement('span', null, '可执行文件路径(绝对路径,优先)'), React.createElement('input', { value: pathDraft, placeholder: '如 C:/.../lua-language-server.exe', onChange: (e) => setPathDraft(e.target.value) })),
|
|
47
|
+
React.createElement('label', null, React.createElement('span', null, '命令名(PATH 内查找)'), React.createElement('input', { value: commandDraft, placeholder: '如 lua-language-server', onChange: (e) => setCommandDraft(e.target.value) })),
|
|
48
|
+
React.createElement('button', { className: 'vsm-primary', disabled: busy === lang.id, onClick: () => onSave(lang.id, pathDraft, commandDraft) }, busy === lang.id ? '保存中…' : '保存配置')),
|
|
49
|
+
React.createElement('div', { className: 'vsm-lsp-hint' }, lang.hint))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 已装扩展卡片:清单 + 卸载/更新。 */
|
|
53
|
+
function InstalledCard({ ext, busy, onUninstall, onUpdate }) {
|
|
54
|
+
return React.createElement('article', { className: 'vsm-mcp-card' },
|
|
55
|
+
React.createElement('div', { className: 'vsm-mcp-card-head' },
|
|
56
|
+
React.createElement('div', { className: 'vsm-mcp-title' },
|
|
57
|
+
React.createElement('span', { className: 'vsm-mcp-dot connected' }),
|
|
58
|
+
React.createElement('span', null, ext.displayName || ext.name),
|
|
59
|
+
React.createElement('span', { className: 'vsm-ext-ver' }, ext.id + ' · v' + ext.version)),
|
|
60
|
+
React.createElement('div', { className: 'vsm-mcp-actions' },
|
|
61
|
+
React.createElement('button', { className: 'vsm-primary vsm-small', disabled: busy === ext.id, onClick: () => onUpdate(ext) }, '更新'),
|
|
62
|
+
React.createElement('button', { className: 'vsm-danger vsm-small', disabled: busy === ext.id, onClick: () => onUninstall(ext) }, '卸载'))),
|
|
63
|
+
ext.description ? React.createElement('div', { className: 'vsm-lsp-hint' }, ext.description) : null)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 市场条目卡片:安装/已装态。 */
|
|
67
|
+
function MarketCard({ item, installed, busy, onInstall }) {
|
|
68
|
+
const has = installed.some((e) => e.id === item.id)
|
|
69
|
+
return React.createElement('article', { className: 'vsm-mcp-card' },
|
|
70
|
+
React.createElement('div', { className: 'vsm-mcp-card-head' },
|
|
71
|
+
React.createElement('div', { className: 'vsm-mcp-title' },
|
|
72
|
+
React.createElement('span', { className: 'vsm-mcp-dot ' + (has ? 'connected' : 'disabled') }),
|
|
73
|
+
React.createElement('span', null, item.displayName || item.name),
|
|
74
|
+
React.createElement('span', { className: 'vsm-ext-ver' }, item.id + ' · v' + item.version)),
|
|
75
|
+
React.createElement('div', { className: 'vsm-mcp-actions' },
|
|
76
|
+
React.createElement('button', { className: 'vsm-primary vsm-small', disabled: has || busy === item.id, onClick: () => onInstall(item) }, has ? '已安装' : '安装'))),
|
|
77
|
+
item.description ? React.createElement('div', { className: 'vsm-lsp-hint' }, item.description) : null)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 语言服务器设置页:服务器状态/配置 + VSIX 扩展(安装/更新/卸载三视图)。
|
|
82
|
+
* @author ddj 2026年08月27号
|
|
83
|
+
*/
|
|
84
|
+
export function LspSettings() {
|
|
85
|
+
const [tab, setTab] = React.useState('servers')
|
|
86
|
+
const [config, setConfig] = React.useState({})
|
|
87
|
+
const [servers, setServers] = React.useState([])
|
|
88
|
+
const [installed, setInstalled] = React.useState([])
|
|
89
|
+
const [updates, setUpdates] = React.useState([])
|
|
90
|
+
const [market, setMarket] = React.useState([])
|
|
91
|
+
const [query, setQuery] = React.useState('')
|
|
92
|
+
const [vsixPath, setVsixPath] = React.useState('')
|
|
93
|
+
const [busy, setBusy] = React.useState('')
|
|
94
|
+
const [error, setError] = React.useState('')
|
|
95
|
+
const [loading, setLoading] = React.useState(true)
|
|
96
|
+
|
|
97
|
+
const refreshServers = React.useCallback(() => {
|
|
98
|
+
Promise.all([rpc('edrv.lsp.configGet', {}), rpc('edrv.lsp.status', {})])
|
|
99
|
+
.then(([cfg, st]) => {
|
|
100
|
+
if (cfg?.ok) setConfig(cfg.config ?? {})
|
|
101
|
+
if (st?.ok) setServers(st.servers ?? [])
|
|
102
|
+
setError('')
|
|
103
|
+
})
|
|
104
|
+
.catch((e) => setError(String(e)))
|
|
105
|
+
}, [])
|
|
106
|
+
|
|
107
|
+
const refreshExt = React.useCallback(() => {
|
|
108
|
+
Promise.all([rpc('edrv.lsp.ext.list', {}), rpc('edrv.lsp.ext.updates', {})])
|
|
109
|
+
.then(([l, u]) => {
|
|
110
|
+
if (l?.ok) setInstalled(l.extensions ?? [])
|
|
111
|
+
if (u?.ok) setUpdates(u.updates ?? [])
|
|
112
|
+
setError('')
|
|
113
|
+
})
|
|
114
|
+
.catch((e) => setError(String(e)))
|
|
115
|
+
}, [])
|
|
116
|
+
|
|
117
|
+
const refresh = React.useCallback(() => {
|
|
118
|
+
setLoading(true)
|
|
119
|
+
Promise.all([refreshServers(), refreshExt()]).finally(() => setLoading(false))
|
|
120
|
+
}, [refreshServers, refreshExt])
|
|
121
|
+
|
|
122
|
+
React.useEffect(() => { refresh() }, [refresh])
|
|
123
|
+
|
|
124
|
+
const switchTab = (next) => {
|
|
125
|
+
setTab(next)
|
|
126
|
+
setError('')
|
|
127
|
+
if (next === 'market' && !query) setQuery('lua language server')
|
|
128
|
+
if (next === 'installed' || next === 'updates') void refreshExt()
|
|
129
|
+
if (next === 'servers') void refreshServers()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const saveLang = (languageId, path, command) => {
|
|
133
|
+
setBusy(languageId)
|
|
134
|
+
setError('')
|
|
135
|
+
rpc('edrv.lsp.configUpdate', { languageId, path, command })
|
|
136
|
+
.then((res) => {
|
|
137
|
+
if (!res?.ok) { setError(res?.error ?? '保存失败'); return }
|
|
138
|
+
setConfig(res.config ?? {})
|
|
139
|
+
})
|
|
140
|
+
.catch((e) => setError(String(e)))
|
|
141
|
+
.finally(() => setBusy(''))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const toggleLang = (languageId, enabled) => {
|
|
145
|
+
setBusy(languageId)
|
|
146
|
+
rpc('edrv.lsp.configUpdate', { languageId, enabled })
|
|
147
|
+
.then((res) => {
|
|
148
|
+
if (res?.ok) { setConfig(res.config ?? {}); void refreshServers() }
|
|
149
|
+
else setError(res?.error ?? '切换失败')
|
|
150
|
+
})
|
|
151
|
+
.catch((e) => setError(String(e)))
|
|
152
|
+
.finally(() => setBusy(''))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const searchMarket = () => {
|
|
156
|
+
if (!query.trim()) return
|
|
157
|
+
setBusy('__market')
|
|
158
|
+
setError('')
|
|
159
|
+
rpc('edrv.lsp.ext.market', { query: query.trim(), size: 12 })
|
|
160
|
+
.then((res) => {
|
|
161
|
+
if (res?.ok) setMarket(res.extensions ?? [])
|
|
162
|
+
else setError(res?.error ?? '搜索失败')
|
|
163
|
+
})
|
|
164
|
+
.catch((e) => setError(String(e)))
|
|
165
|
+
.finally(() => setBusy(''))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const installMarket = (item) => {
|
|
169
|
+
setBusy(item.id)
|
|
170
|
+
setError('')
|
|
171
|
+
rpc('edrv.lsp.ext.install', { namespace: item.namespace, name: item.name })
|
|
172
|
+
.then((res) => {
|
|
173
|
+
if (!res?.ok) { setError(res?.error ?? '安装失败'); return }
|
|
174
|
+
void refreshExt()
|
|
175
|
+
})
|
|
176
|
+
.catch((e) => setError(String(e)))
|
|
177
|
+
.finally(() => setBusy(''))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const installLocal = () => {
|
|
181
|
+
if (!vsixPath.trim()) return
|
|
182
|
+
setBusy('__vsix')
|
|
183
|
+
setError('')
|
|
184
|
+
rpc('edrv.lsp.ext.install', { vsixPath: vsixPath.trim() })
|
|
185
|
+
.then((res) => {
|
|
186
|
+
if (!res?.ok) { setError(res?.error ?? '安装失败'); return }
|
|
187
|
+
setVsixPath('')
|
|
188
|
+
void refreshExt()
|
|
189
|
+
})
|
|
190
|
+
.catch((e) => setError(String(e)))
|
|
191
|
+
.finally(() => setBusy(''))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const uninstallExt = (ext) => {
|
|
195
|
+
if (!window.confirm('卸载扩展 ' + ext.id + '?')) return
|
|
196
|
+
setBusy(ext.id)
|
|
197
|
+
setError('')
|
|
198
|
+
rpc('edrv.lsp.ext.uninstall', { id: ext.id })
|
|
199
|
+
.then((res) => {
|
|
200
|
+
if (!res?.ok) setError(res?.error ?? '卸载失败')
|
|
201
|
+
void refreshExt()
|
|
202
|
+
})
|
|
203
|
+
.catch((e) => setError(String(e)))
|
|
204
|
+
.finally(() => setBusy(''))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const updateExt = (ext) => {
|
|
208
|
+
setBusy(ext.id)
|
|
209
|
+
setError('')
|
|
210
|
+
rpc('edrv.lsp.ext.update', { id: ext.id })
|
|
211
|
+
.then((res) => {
|
|
212
|
+
if (!res?.ok) setError(res?.error ?? '更新失败')
|
|
213
|
+
void refreshExt()
|
|
214
|
+
})
|
|
215
|
+
.catch((e) => setError(String(e)))
|
|
216
|
+
.finally(() => setBusy(''))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (loading) return React.createElement('div', { className: 'vsm-mcp-empty' }, '正在读取语言服务器状态…')
|
|
220
|
+
const nav = React.createElement('nav', { className: 'vsm-mcp-tabs' },
|
|
221
|
+
TABS.map((t) => React.createElement('button', { key: t.id, className: tab === t.id ? 'active' : '', onClick: () => switchTab(t.id) }, t.label)))
|
|
222
|
+
const header = React.createElement('div', { className: 'vsm-mcp-header' },
|
|
223
|
+
React.createElement('div', null,
|
|
224
|
+
React.createElement('h2', null, '语言服务器(LSP)'),
|
|
225
|
+
React.createElement('p', null, '代码索引 / 引用查找 / 动态跳转;可安装 VSIX 语言服务器扩展(Open VSX 市场 / 本地 .vsix)。')),
|
|
226
|
+
React.createElement('button', { className: 'vsm-primary', onClick: refresh }, '↻ 刷新'))
|
|
227
|
+
const banner = error ? React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error) : null
|
|
228
|
+
|
|
229
|
+
let body = null
|
|
230
|
+
if (tab === 'servers') {
|
|
231
|
+
body = React.createElement('div', null,
|
|
232
|
+
LANGUAGES.map((lang) => React.createElement(LangCard, {
|
|
233
|
+
key: lang.id, lang,
|
|
234
|
+
config: config[lang.id] ?? {},
|
|
235
|
+
status: servers.find((s) => s.languageId === lang.id),
|
|
236
|
+
busy, onToggle: toggleLang, onSave: saveLang,
|
|
237
|
+
})),
|
|
238
|
+
React.createElement('div', { className: 'vsm-lsp-hint' }, '提示:打开 Lua/C# 文件即自动(惰性)启动对应语言服务器;未配置时编辑器功能不受影响,大纲回退内置解析。'))
|
|
239
|
+
} else if (tab === 'installed') {
|
|
240
|
+
body = React.createElement('div', null,
|
|
241
|
+
React.createElement('div', { className: 'vsm-lsp-hint' }, '已安装的语言服务器扩展(存于 ~/.dsh/dsh-vscode-mode/extensions/)。语言服务器被自动发现为「扩展」源。'),
|
|
242
|
+
installed.length ? installed.map((ext) => React.createElement(InstalledCard, { key: ext.id, ext, busy, onUninstall: uninstallExt, onUpdate: updateExt }))
|
|
243
|
+
: React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有安装扩展(去「市场」或指定本地 .vsix)'))
|
|
244
|
+
} else if (tab === 'market') {
|
|
245
|
+
body = React.createElement('div', null,
|
|
246
|
+
React.createElement('div', { className: 'vsm-lsp-form' },
|
|
247
|
+
React.createElement('label', null, React.createElement('span', null, '搜索 Open VSX 市场'), React.createElement('input', { value: query, placeholder: '如 sumneko.lua / lua language server', onChange: (e) => setQuery(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter') searchMarket() } })),
|
|
248
|
+
React.createElement('button', { className: 'vsm-primary', disabled: busy === '__market', onClick: searchMarket }, busy === '__market' ? '搜索中…' : '搜索')),
|
|
249
|
+
React.createElement('div', { className: 'vsm-lsp-form' },
|
|
250
|
+
React.createElement('label', null, React.createElement('span', null, '安装本地 .vsix 文件(服务器绝对路径)'), React.createElement('input', { value: vsixPath, placeholder: '如 C:/Downloads/sumneko.lua-3.19.1.vsix', onChange: (e) => setVsixPath(e.target.value) })),
|
|
251
|
+
React.createElement('button', { className: 'vsm-primary', disabled: busy === '__vsix', onClick: installLocal }, busy === '__vsix' ? '安装中…' : '安装本地 .vsix')),
|
|
252
|
+
React.createElement('div', { className: 'vsm-lsp-hint' }, '市场安装会下载并解包 VSIX 到扩展目录;含语言服务器(如 sumneko.lua)的扩展会被自动发现。'),
|
|
253
|
+
market.map((item) => React.createElement(MarketCard, { key: item.id, item, installed, busy, onInstall: installMarket })))
|
|
254
|
+
} else {
|
|
255
|
+
body = React.createElement('div', null,
|
|
256
|
+
React.createElement('div', { className: 'vsm-lsp-hint' }, '已装扩展与 Open VSX 最新版的差异。'),
|
|
257
|
+
updates.length ? updates.map((u) => React.createElement('article', { key: u.id, className: 'vsm-mcp-card' },
|
|
258
|
+
React.createElement('div', { className: 'vsm-mcp-card-head' },
|
|
259
|
+
React.createElement('div', { className: 'vsm-mcp-title' },
|
|
260
|
+
React.createElement('span', { className: 'vsm-mcp-dot error' }),
|
|
261
|
+
React.createElement('span', null, u.displayName || u.id),
|
|
262
|
+
React.createElement('span', { className: 'vsm-ext-ver' }, 'v' + u.current + ' → v' + u.latest)),
|
|
263
|
+
React.createElement('div', { className: 'vsm-mcp-actions' },
|
|
264
|
+
React.createElement('button', { className: 'vsm-primary vsm-small', disabled: busy === u.id, onClick: () => updateExt(u) }, busy === u.id ? '更新中…' : '更新')))))
|
|
265
|
+
: React.createElement('div', { className: 'vsm-mcp-empty' }, '全部已是最新'))
|
|
266
|
+
}
|
|
267
|
+
return React.createElement('div', null, header, nav, banner, body)
|
|
268
|
+
}
|
|
@@ -12,6 +12,7 @@ import '../styles/mcp.css'
|
|
|
12
12
|
import { availableOpeners, AUTO_OPEN_TOOL } from '../fileOpeners.js'
|
|
13
13
|
import { SettingsContext } from '../settingsContext.js'
|
|
14
14
|
import { KeybindingsPanel } from './KeybindingsPanel.js'
|
|
15
|
+
import { LspSettings } from './LspSettings.js'
|
|
15
16
|
import { PerfSettings } from './PerfSettings.js'
|
|
16
17
|
|
|
17
18
|
const EMPTY = { serverName: '', transport: 'stdio', command: '', args: '', cwd: '', url: '', headers: '' }
|
|
@@ -322,12 +323,13 @@ export function McpSettings(props) {
|
|
|
322
323
|
else if (tab === 'mine') body = React.createElement('div', null, servers.length ? servers.map((s) => React.createElement(ServerCard, { key: s.id, server: s, onRefresh: refreshGlobal, onToggle: toggleGlobal, onRemove: removeGlobal })) : React.createElement('div', { className: 'vsm-mcp-empty' }, '还没有配置全局 MCP'))
|
|
323
324
|
else if (tab === 'projects') body = projectBody
|
|
324
325
|
else if (tab === 'compat') body = React.createElement(CompatSection, { getSummary: compatSummary })
|
|
326
|
+
else if (tab === 'lsp') body = React.createElement(LspSettings, null)
|
|
325
327
|
else if (tab === 'perf') body = React.createElement(PerfSettings, null)
|
|
326
328
|
else body = marketBody
|
|
327
329
|
const form = showForm ? React.createElement(McpForm, { title: '添加全局 MCP', draft, busy, edit, save: saveGlobal, close: () => setShowForm(false) }) : projectForm ? React.createElement(McpForm, { title: '添加项目 MCP · ' + projectForm.title, draft, busy, edit, save: saveProject, close: () => setProjectForm(null) }) : null
|
|
328
330
|
return React.createElement('section', { className: 'vsm-mcp-page' },
|
|
329
331
|
React.createElement('header', { className: 'vsm-mcp-header' }, React.createElement('div', null, React.createElement('h2', null, 'VSCodeMode'), React.createElement('p', null, '管理当前 profile 与各项目的 Model Context Protocol 服务。')), React.createElement('button', { className: 'vsm-primary', onClick: () => { setDraft(EMPTY); setShowForm(true) } }, '+ 添加全局 MCP')),
|
|
330
|
-
React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'general' ? 'active' : '', onClick: () => setTab('general') }, '通用'), React.createElement('button', { className: tab === 'keys' ? 'active' : '', onClick: () => setTab('keys') }, '快捷键'), React.createElement('button', { className: tab === 'mine' ? 'active' : '', onClick: () => setTab('mine') }, '我的 MCP'), React.createElement('button', { className: tab === 'projects' ? 'active' : '', onClick: () => setTab('projects') }, '项目 MCP'), React.createElement('button', { className: tab === 'perf' ? 'active' : '', onClick: () => setTab('perf') }, '性能优化'), React.createElement('button', { className: tab === 'market' ? 'active' : '', onClick: () => setTab('market') }, 'MCP 市场'), React.createElement('button', { className: tab === 'compat' ? 'active' : '', onClick: () => setTab('compat') }, '兼容性')),
|
|
332
|
+
React.createElement('nav', { className: 'vsm-mcp-tabs' }, React.createElement('button', { className: tab === 'general' ? 'active' : '', onClick: () => setTab('general') }, '通用'), React.createElement('button', { className: tab === 'keys' ? 'active' : '', onClick: () => setTab('keys') }, '快捷键'), React.createElement('button', { className: tab === 'mine' ? 'active' : '', onClick: () => setTab('mine') }, '我的 MCP'), React.createElement('button', { className: tab === 'projects' ? 'active' : '', onClick: () => setTab('projects') }, '项目 MCP'), React.createElement('button', { className: tab === 'lsp' ? 'active' : '', onClick: () => setTab('lsp') }, '语言服务器'), React.createElement('button', { className: tab === 'perf' ? 'active' : '', onClick: () => setTab('perf') }, '性能优化'), React.createElement('button', { className: tab === 'market' ? 'active' : '', onClick: () => setTab('market') }, 'MCP 市场'), React.createElement('button', { className: tab === 'compat' ? 'active' : '', onClick: () => setTab('compat') }, '兼容性')),
|
|
331
333
|
error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
|
|
332
334
|
body,
|
|
333
335
|
form,
|
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
|
/**
|