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.
@@ -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,
@@ -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
  /**