dsh-vscode-mode 0.1.30 → 0.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/lib/client.js +1439 -46
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +3519 -222
  6. package/lib/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/capture.ts +3 -0
  9. package/src/client/events.ts +11 -0
  10. package/src/client/index.ts +20 -2
  11. package/src/client/monaco/lsp/index.ts +30 -0
  12. package/src/client/monaco/lsp/lspClient.ts +192 -0
  13. package/src/client/monaco/lsp/providers.ts +229 -0
  14. package/src/client/outline/OutlinePanel.tsx +1 -1
  15. package/src/client/outline/lspSource.ts +49 -0
  16. package/src/client/outline/parse.ts +31 -1
  17. package/src/client/outline/types.ts +2 -0
  18. package/src/client/paths.ts +36 -0
  19. package/src/client/sidebar/panels/FileExplorer.ts +95 -30
  20. package/src/client/sidebar/panels/SearchPanel.ts +11 -2
  21. package/src/client/state/explorerCache.ts +3 -1
  22. package/src/client/state/explorerEntriesCache.ts +172 -0
  23. package/src/client/state/viewStateCache.ts +3 -1
  24. package/src/client/styles/editor.css +16 -0
  25. package/src/client/styles/mcp.css +63 -0
  26. package/src/client/ui/EditorView.ts +61 -6
  27. package/src/client/ui/LspSettings.ts +268 -0
  28. package/src/client/ui/McpSettings.ts +5 -1
  29. package/src/client/ui/PerfSettings.ts +242 -0
  30. package/src/client/ui/PerfSizeBadge.ts +54 -0
  31. package/src/compat.ts +3 -2
  32. package/src/fileOpenSettings.ts +1 -0
  33. package/src/index.ts +25 -2
  34. package/src/lsp/client.ts +205 -0
  35. package/src/lsp/config.ts +107 -0
  36. package/src/lsp/extmgr.ts +208 -0
  37. package/src/lsp/jsonrpc.ts +86 -0
  38. package/src/lsp/manager.ts +133 -0
  39. package/src/lsp/providers.ts +235 -0
  40. package/src/lsp/rpc.ts +334 -0
  41. package/src/lsp/server.ts +346 -0
  42. package/src/lsp/settings.ts +44 -0
  43. package/src/lsp/transport.ts +143 -0
  44. package/src/lsp/uri.ts +62 -0
  45. package/src/lsp/zip.ts +82 -0
  46. package/src/paths.ts +284 -0
  47. package/src/perf.ts +340 -0
  48. package/src/perfPatch.ts +76 -0
  49. package/src/routes.ts +7 -12
  50. package/src/rpc.ts +184 -36
  51. package/src/search/content.ts +14 -7
  52. package/src/search/ripgrep.ts +54 -1
  53. package/src/search/types.ts +4 -1
  54. package/src/shared/lsp.ts +138 -0
  55. package/src/shared/rpc.ts +103 -2
  56. package/src/store.ts +2 -2
  57. package/src/treeIndex.ts +397 -0
  58. package/src/treeRead.ts +87 -0
@@ -22,6 +22,9 @@ import { revealInExplorer as revealPathInExplorer } from '../fileReveal.js'
22
22
  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
+ 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'
25
28
 
26
29
  /**
27
30
  * 中央编辑区:文件页签(脏点/关闭/打开路径)+ Ctrl+P 搜索 + Monaco 编辑器 +
@@ -46,6 +49,7 @@ export function EditorView(props) {
46
49
  const [content, setContent] = React.useState(null)
47
50
  const [contentPath, setContentPath] = React.useState(null)
48
51
  const [status, setStatus] = React.useState('')
52
+ const [lspServers, setLspServers] = React.useState([])
49
53
  const [error, setError] = React.useState(null)
50
54
  const [loadError, setLoadError] = React.useState(null)
51
55
  const [loadStage, setLoadStage] = React.useState({ progress: 0, message: '准备加载编辑器…' })
@@ -64,7 +68,7 @@ export function EditorView(props) {
64
68
  const [focusRequest, setFocusRequest] = React.useState(0) // 外部差异聚焦请求版本
65
69
  const kbVersion = useKeybindingsVersion() // 快捷键配置版本(tooltip 文案随键位刷新)
66
70
  const [hintDismissed, setHintDismissed] = React.useState(() => {
67
- try { return localStorage.getItem('edrv.side-hint-dismissed') === '1' } catch { return false }
71
+ try { return localStorage.getItem(CACHE_KEY.sideHint) === '1' } catch { return false }
68
72
  }) // 侧边栏引导条是否已关闭
69
73
  const editorRef = React.useRef(null)
70
74
  const viewRootRef = React.useRef(null)
@@ -264,6 +268,14 @@ export function EditorView(props) {
264
268
  return () => { clearInterval(t); window.removeEventListener('edrv:refresh', onRefresh) }
265
269
  }, [sessionId])
266
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
+
267
279
  React.useEffect(() => {
268
280
  const onOpen = (e) => {
269
281
  const p = e?.detail?.path
@@ -271,6 +283,13 @@ export function EditorView(props) {
271
283
  if (e?.detail?.focusDiff === true) {
272
284
  pendingFocusRef.current = { path: p, region: null }
273
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
274
293
  }
275
294
  addTab(p, true)
276
295
  }
@@ -281,9 +300,12 @@ export function EditorView(props) {
281
300
  }
282
301
  window.addEventListener('edrv:open-editor', onOpen)
283
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)
284
305
  return () => {
285
306
  window.removeEventListener('edrv:open-editor', onOpen)
286
307
  window.removeEventListener('edrv:show-launcher', onShowLauncher)
308
+ window.removeEventListener('edrv:status', onStatus)
287
309
  }
288
310
  }, [sessionId])
289
311
 
@@ -356,7 +378,7 @@ export function EditorView(props) {
356
378
  bootRef.current = true
357
379
  viewStatesRef.current = viewStatesLoad(sessionId)
358
380
  try {
359
- const raw = localStorage.getItem('edrv.editor.v2.' + String(sessionId))
381
+ const raw = localStorage.getItem(CACHE_KEY.editor + String(sessionId))
360
382
  if (raw) {
361
383
  const saved = JSON.parse(raw)
362
384
  if (Array.isArray(saved.tabs) && saved.tabs.length) {
@@ -369,12 +391,12 @@ export function EditorView(props) {
369
391
 
370
392
  React.useEffect(() => {
371
393
  if (!sessionId) return
372
- try { localStorage.setItem('edrv.editor.v2.' + String(sessionId), JSON.stringify({ tabs: tabs.map((t) => t.path), active })) }
394
+ try { localStorage.setItem(CACHE_KEY.editor + String(sessionId), JSON.stringify({ tabs: tabs.map((t) => t.path), active })) }
373
395
  catch (e) { /* 忽略 */ }
374
396
  }, [tabs, active, sessionId])
375
397
 
376
398
  // 侧边栏状态:恢复(显隐/宽度/激活面板);侧栏形态独立键(默认收起,不共享页签形态偏好)
377
- const sidebarKey = 'edrv.sidebar.' + (layout === 'side' ? 'side.' : '') + String(sessionId)
399
+ const sidebarKey = CACHE_KEY.sidebar + (layout === 'side' ? 'side.' : '') + String(sessionId)
378
400
  React.useEffect(() => {
379
401
  if (!sessionId) return
380
402
  try {
@@ -542,6 +564,7 @@ export function EditorView(props) {
542
564
  flushSave()
543
565
  saveViewState(active)
544
566
  diffRendererRef.current?.dispose?.()
567
+ hideReferencesOverlay()
545
568
  if (editorRef.current) { editorRef.current.dispose(); editorRef.current = null }
546
569
  for (const m of modelsRef.current.values()) m.dispose()
547
570
  modelsRef.current.clear()
@@ -628,6 +651,21 @@ export function EditorView(props) {
628
651
  precondition: 'editorTextFocus',
629
652
  run: (edx) => menuHandlers()?.openInExplorer(pathOf(edx)),
630
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)
631
669
  // hover 差异块 → 浮出 Keep/Undo(req:鼠标移到编辑区差异块时显示)
632
670
  // 防闪烁:① 区域不变不 setState(浮窗锚定差异块起始行,不跟随鼠标);② 延迟隐藏;
633
671
  // ③ 浮窗自身 onMouseEnter 取消隐藏计时(鼠标在浮窗与编辑器间移动不闪)。
@@ -981,6 +1019,22 @@ export function EditorView(props) {
981
1019
  (cursor ? React.createElement('span', { className: 'edrv-pb-meta' }, cursor) : null),
982
1020
  (status ? React.createElement('span', { className: 'edrv-pb-meta edrv-pb-status' }, status) : null))
983
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
+
984
1038
  const tabRow = React.createElement('div', { style: { display: 'flex', alignItems: 'center', background: 'var(--dsw-alias-bg-layer-1,transparent)', flexShrink: 0 } },
985
1039
  tabsEl,
986
1040
  (sum.totalFiles > 0
@@ -1152,7 +1206,7 @@ export function EditorView(props) {
1152
1206
  // 侧边栏引导条(仅旧页签形态且未装 betterSidebar 时显示;可复制安装命令、可关闭)
1153
1207
  const dismissHint = () => {
1154
1208
  setHintDismissed(true)
1155
- try { localStorage.setItem('edrv.side-hint-dismissed', '1') } catch (e) { /* 忽略 */ }
1209
+ try { localStorage.setItem(CACHE_KEY.sideHint, '1') } catch (e) { /* 忽略 */ }
1156
1210
  }
1157
1211
  const copyInstallCmd = () => {
1158
1212
  if (navigator.clipboard?.writeText) navigator.clipboard.writeText(SIDEBAR_INSTALL_CMD).catch(() => {})
@@ -1168,7 +1222,8 @@ export function EditorView(props) {
1168
1222
  pathBar,
1169
1223
  tabRow,
1170
1224
  sideHintEl,
1171
- editorArea)
1225
+ editorArea,
1226
+ lspFooter)
1172
1227
  // 编辑器根节点按 composer 顶部边界动态限高,底部对话区域继续由 DSH 原生渲染。
1173
1228
  // 侧栏形态由面板容器给高(100%),不做 composer 几何同步。
1174
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,8 @@ 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'
16
+ import { PerfSettings } from './PerfSettings.js'
15
17
 
16
18
  const EMPTY = { serverName: '', transport: 'stdio', command: '', args: '', cwd: '', url: '', headers: '' }
17
19
 
@@ -321,11 +323,13 @@ export function McpSettings(props) {
321
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'))
322
324
  else if (tab === 'projects') body = projectBody
323
325
  else if (tab === 'compat') body = React.createElement(CompatSection, { getSummary: compatSummary })
326
+ else if (tab === 'lsp') body = React.createElement(LspSettings, null)
327
+ else if (tab === 'perf') body = React.createElement(PerfSettings, null)
324
328
  else body = marketBody
325
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
326
330
  return React.createElement('section', { className: 'vsm-mcp-page' },
327
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')),
328
- 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 === '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') }, '兼容性')),
329
333
  error && React.createElement('div', { className: 'vsm-mcp-error vsm-mcp-banner' }, error),
330
334
  body,
331
335
  form,