dsh-vscode-mode 0.1.36 → 0.1.42

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.1.36",
3
+ "version": "0.1.42",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
5
5
  "keywords": [
6
6
  "dsh",
@@ -4,6 +4,7 @@
4
4
  * 迁移自原 src/client/index.ts 的 MONACO_BASE/LANG_BY_EXT/langOf/loadMonaco,语义不改。
5
5
  * 作者 ddj 2026-08-20
6
6
  */
7
+ import { applyTheme, registerThemes } from './theme.js'
7
8
 
8
9
  export const MONACO_BASE = '/edrv/vendor/monaco/vs'
9
10
  let monacoPromise = null
@@ -58,7 +59,12 @@ export const LANG_BY_EXT = {
58
59
  py: 'python', sh: 'shell', bash: 'shell', zsh: 'shell', ps1: 'powershell',
59
60
  lua: 'lua', java: 'java', c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', cxx: 'cpp', hpp: 'cpp',
60
61
  cs: 'csharp', go: 'go', rs: 'rust', rb: 'ruby', php: 'php', sql: 'sql',
61
- swift: 'swift', kt: 'kotlin', kts: 'kotlin', dart: 'dart', dockerfile: 'dockerfile'
62
+ swift: 'swift', kt: 'kotlin', kts: 'kotlin', dart: 'dart', dockerfile: 'dockerfile',
63
+ // 工程/配置文件(拿到正确语法分色,而非 plaintext)
64
+ csproj: 'xml', props: 'xml', targets: 'xml', plist: 'xml',
65
+ lua51: 'lua', luac: 'lua',
66
+ gitattributes: 'ini', editorconfig: 'ini', env: 'ini', properties: 'ini',
67
+ json5: 'jsonc', log: 'plaintext', txt: 'plaintext',
62
68
  }
63
69
 
64
70
  /**
@@ -96,6 +102,11 @@ export function loadMonaco(onProgress) {
96
102
  publishStage('core', MONACO_STAGES.core.progress, MONACO_STAGES.core.message)
97
103
  window.require(['vs/editor/editor.main'], () => {
98
104
  publishStage('ready', MONACO_STAGES.ready.progress, MONACO_STAGES.ready.message)
105
+ // 分色主题注册 + 应用(rich token 配色,替掉内置基础 vs 的少层次着色)
106
+ try {
107
+ registerThemes(window.monaco)
108
+ applyTheme(window.monaco)
109
+ } catch (error) { /* 主题失败不阻塞编辑器 */ }
99
110
  resolve(window.monaco)
100
111
  }, (err) => fail(new Error('Monaco 模块加载失败:' + String(err))))
101
112
  } catch (error) {
@@ -113,6 +113,14 @@ export async function fetchHover(path, text, position) {
113
113
  return res.hover || null
114
114
  }
115
115
 
116
+ /** 查询当前文档的 semantic tokens(host 已归一化 legend 与 delta data)。 */
117
+ export async function fetchSemanticTokens(path, text) {
118
+ await syncDoc(path, text, true)
119
+ const res = await rpc('edrv.lsp.semanticTokens', { sessionId, path }).catch(() => null)
120
+ if (!res || !res.ok || !res.tokens || !Array.isArray(res.tokens.data)) return null
121
+ return res.tokens
122
+ }
123
+
116
124
  /** 拉取服务器状态(带 5s 缓存)。 */
117
125
  export async function refreshStatus(force = false) {
118
126
  if (!force && Date.now() - statusCache.at < 1500) return statusCache.servers
@@ -148,6 +156,10 @@ function toClientLocation(loc) {
148
156
  Math.max(1, (end.line ?? 0) + 1),
149
157
  Math.max(1, (end.character ?? 0) + 1),
150
158
  ),
159
+ // 保留原始 LSP uri/root:后续跳转不能把 Monaco edrv Uri 当作磁盘路径
160
+ lspUri: loc.uri,
161
+ lspRoot: loc.root,
162
+ lspRange: loc.range,
151
163
  }
152
164
  }
153
165
 
@@ -161,32 +173,74 @@ export function toEdrvUri(uri, root) {
161
173
 
162
174
  /** 将 LSP 目标解析为当前工作区相对路径(跨文件跳转与 model URI 对齐)。 */
163
175
  export function targetOpenPath(loc) {
164
- return relativeLspPath(loc?.uri, loc?.root)
176
+ return relativeLspPath(loc?.lspUri ?? loc?.uri, loc?.lspRoot ?? loc?.root)
165
177
  }
166
178
 
167
- /** file URI/裸路径 → root 下的 / 分隔相对路径;root 不匹配时保留规范化绝对路径。 */
168
- function relativeLspPath(uri, root) {
179
+ /**
180
+ * 归一化 Location 的 uri → 工作区路径(剥离 scheme,不补盘符)。
181
+ * 三种输入形态(缺一即导致 scheme 泄漏进文件路径 → 跳转"文件不存在"):
182
+ * - Monaco Uri 对象(toClientLocation 产物,scheme=edrv)→ 取 .path(已是工作区相对路径)
183
+ * - 'edrv:///x' 字符串 → 取路径部分
184
+ * - 'file:///D:/x' → 去 authority 空段保留盘符
185
+ * @author ddj 2026年08月28号
186
+ * @param uri Monaco Uri 对象 / file:// 或 edrv:// URI / 裸路径
187
+ * @returns / 分隔的路径(edrv 相对路径 或 带盘符绝对路径)
188
+ */
189
+ export function lspUriToAbs(uri) {
169
190
  if (!uri) return ''
170
- let path = String(uri)
191
+ const isObject = typeof uri === 'object'
192
+ // Monaco Uri 对象:优先取其 path(已剥离 scheme)
193
+ const raw = isObject && typeof uri.path === 'string' ? uri.path : String(uri)
194
+ let path = raw
171
195
  if (path.startsWith('file://')) {
172
- try { path = decodeURIComponent(path.replace(/^file:\/\//, '')) } catch (error) { path = path.replace(/^file:\/\//, '') }
196
+ try { path = decodeURIComponent(path.replace(/^file:\/\/\/?/, '')) } catch (error) { path = path.replace(/^file:\/\/\/?/, '') }
197
+ // file:///D:/x → D:/x(去 authority 空段保留盘符);/home/x 保留前导 /(Unix 绝对路径)
198
+ return path.replace(/\\/g, '/').replace(/^\/([A-Za-z]:)/, '$1')
199
+ }
200
+ if (path.startsWith('edrv://')) {
201
+ try { path = decodeURIComponent(path.replace(/^edrv:\/\/\/?/, '')) } catch (error) { path = path.replace(/^edrv:\/\/\/?/, '') }
173
202
  }
174
- path = path.replace(/\\/g, '/').replace(/^\/([A-Za-z]:)/, '$1')
203
+ // edrv Uri(对象或字符串)→ 工作区相对路径:必须剥前导 /,否则 host 按绝对路径解析丢 cwd
204
+ if (isObject ? uri.scheme === 'edrv' : raw.startsWith('edrv://')) {
205
+ return path.replace(/\\/g, '/').replace(/^\/+/, '')
206
+ }
207
+ return path.replace(/\\/g, '/').replace(/^\/([A-Za-z]:)/, '$1')
208
+ }
209
+
210
+ /**
211
+ * file URI/裸路径 → root 下的 / 分隔相对路径。
212
+ * root 匹配返回工作区相对路径;root 不匹配返回完整绝对路径(保留盘符),
213
+ * 供 edrv.read 按绝对路径解析(原先错误剥掉首个 / 导致跳转"文件不存在")。
214
+ * @author ddj 2026年08月28号
215
+ * @param uri file:// URI 或裸路径
216
+ * @param root 工作区根(可选)
217
+ * @returns 可 edrv.read 的路径
218
+ */
219
+ function relativeLspPath(uri, root) {
220
+ if (!uri) return ''
221
+ const path = lspUriToAbs(uri)
175
222
  const normalizedRoot = root ? String(root).replace(/\\/g, '/').replace(/^\/([A-Za-z]:)/, '$1').replace(/\/+$/, '') : ''
176
223
  const lowerPath = path.toLowerCase()
177
224
  const lowerRoot = normalizedRoot.toLowerCase()
178
225
  if (normalizedRoot && (lowerPath === lowerRoot || lowerPath.startsWith(lowerRoot + '/'))) {
179
226
  return path.slice(normalizedRoot.length).replace(/^\/+/, '')
180
227
  }
181
- return path.replace(/^\/+/, '')
228
+ return path
182
229
  }
183
230
 
184
231
  /** 目标行/列(0-based LSP → 1-based Monaco)。 */
185
232
  export function targetMonoPosition(loc) {
186
- const range = loc.range || {}
187
- const start = range.start || {}
233
+ // 优先使用原始 LSP range(0-based);无原始值时兼容 Monaco Range(1-based)
234
+ const lspRange = loc?.lspRange
235
+ if (lspRange?.start) {
236
+ return {
237
+ line: Math.max(1, (lspRange.start.line ?? 0) + 1),
238
+ column: Math.max(1, (lspRange.start.character ?? 0) + 1),
239
+ }
240
+ }
241
+ const range = loc?.range ?? {}
188
242
  return {
189
- line: Math.max(1, (start.line ?? 0) + 1),
190
- column: Math.max(1, (start.character ?? 0) + 1),
243
+ line: Math.max(1, range.startLineNumber ?? 1),
244
+ column: Math.max(1, range.startColumn ?? 1),
191
245
  }
192
246
  }
@@ -8,10 +8,16 @@
8
8
  */
9
9
  import {
10
10
  pathOfModel, syncDoc, closeDoc, findDefinition, findReferences,
11
- fetchDocumentSymbols, fetchHover, targetOpenPath, targetMonoPosition, lspStatusFor,
11
+ fetchDocumentSymbols, fetchHover, fetchSemanticTokens,
12
+ targetOpenPath, targetMonoPosition, lspStatusFor, refreshStatus,
12
13
  } from './lspClient.js'
14
+ import { LSP_SEMANTIC_TOKEN_MODIFIERS, LSP_SEMANTIC_TOKEN_TYPES } from '../../../shared/lsp.js'
13
15
 
14
16
  const LSP_LANGS = ['lua', 'csharp']
17
+ const SEMANTIC_LEGEND = {
18
+ tokenTypes: [...LSP_SEMANTIC_TOKEN_TYPES],
19
+ tokenModifiers: [...LSP_SEMANTIC_TOKEN_MODIFIERS],
20
+ }
15
21
  let registered = false
16
22
  const disposables = []
17
23
  let overlayEl = null
@@ -48,6 +54,22 @@ export function registerLspProviders(monaco) {
48
54
  disposables.push(monaco.editor.onDidCreateModel(attachModel))
49
55
  for (const model of monaco.editor.getModels()) attachModel(model)
50
56
 
57
+ // —— 重新检测事件:保存配置/切换启用/手动重检测后,把已打开模型重新同步给 host ——
58
+ // (host 已重置该语言 server,下一次 sync 会按最新配置重新 acquire,无需重开文件)
59
+ const onRedetect = (event) => {
60
+ const languageId = event?.detail?.languageId
61
+ if (!languageId) return
62
+ for (const model of monaco.editor.getModels()) {
63
+ const path = pathOfModel(model)
64
+ if (!path) continue
65
+ if (model.getLanguageId && model.getLanguageId() !== languageId) continue
66
+ void syncDoc(path, model.getValue(), true)
67
+ }
68
+ void refreshStatus(true)
69
+ }
70
+ window.addEventListener('edrv:lsp-redetect', onRedetect)
71
+ disposables.push(() => window.removeEventListener('edrv:lsp-redetect', onRedetect))
72
+
51
73
  // —— 数据 provider ——
52
74
  disposables.push(
53
75
  monaco.languages.registerDefinitionProvider(LSP_LANGS, {
@@ -87,6 +109,17 @@ export function registerLspProviders(monaco) {
87
109
  }
88
110
  },
89
111
  }),
112
+ monaco.languages.registerDocumentSemanticTokensProvider(LSP_LANGS, {
113
+ getLegend: () => SEMANTIC_LEGEND,
114
+ provideDocumentSemanticTokens: async (model, token) => {
115
+ const path = pathOfModel(model)
116
+ if (!path || token?.isCancellationRequested) return { data: new Uint32Array() }
117
+ const result = await fetchSemanticTokens(path, model.getValue())
118
+ if (!result || token?.isCancellationRequested) return { data: new Uint32Array() }
119
+ return { data: Uint32Array.from(result.data) }
120
+ },
121
+ releaseDocumentSemanticTokens: () => {},
122
+ }),
90
123
  )
91
124
  }
92
125
 
@@ -0,0 +1,229 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — Monaco 语法分色主题(rich token 配色)。
4
+ * 编辑器原先硬编码 'vs'(内置基础主题),token 分色层次少;
5
+ * 这里定义 edrv-dark / edrv-light 两套完整 token 配色(对齐 VSCode Dark+ 语义分层),
6
+ * 并随 DSH 明暗主题自动切换(跟随文档根 data 主题属性 / prefers-color-scheme)。
7
+ * 作者 ddj 2026-08-28
8
+ */
9
+
10
+ export const EDRV_DARK = 'edrv-dark'
11
+ export const EDRV_LIGHT = 'edrv-light'
12
+ export const EDRV_FALLBACK = 'vs'
13
+
14
+ /** 语法分色(语义分层:关键字/字符串/数字/注释/函数/变量/类型/运算符/正则/标签/属性…)。 */
15
+ const DARK_RULES = [
16
+ { token: '', foreground: 'd4d4d4', background: '1e1e1e' },
17
+ { token: 'comment', foreground: '6a9955', fontStyle: 'italic' },
18
+ { token: 'comment.doc', foreground: '7ca97c', fontStyle: 'italic' },
19
+ { token: 'keyword', foreground: 'c586c0' },
20
+ { token: 'keyword.control', foreground: 'c586c0' },
21
+ { token: 'keyword.operator', foreground: 'd4d4d4' },
22
+ { token: 'keyword.json', foreground: '9cdcfe' },
23
+ { token: 'string', foreground: 'ce9178' },
24
+ { token: 'string.escape', foreground: 'd7ba7d' },
25
+ { token: 'string.key.json', foreground: '9cdcfe' },
26
+ { token: 'string.value.json', foreground: 'ce9178' },
27
+ { token: 'string.yaml', foreground: 'ce9178' },
28
+ { token: 'number', foreground: 'b5cea8' },
29
+ { token: 'number.hex', foreground: 'b5cea8' },
30
+ { token: 'regexp', foreground: 'd16969' },
31
+ { token: 'type', foreground: '4ec9b0' },
32
+ { token: 'type.identifier', foreground: '4ec9b0' },
33
+ { token: 'identifier', foreground: 'd4d4d4' },
34
+ { token: 'variable', foreground: '9cdcfe' },
35
+ { token: 'variable.global', foreground: 'ff6699' },
36
+ { token: 'variable.local', foreground: '9cdcfe' },
37
+ { token: 'variable.mutable', foreground: '9cdcfe' },
38
+ { token: 'variable.predefined', foreground: '4fc1ff' },
39
+ { token: 'variable.parameter', foreground: '00bfae' },
40
+ { token: 'parameter', foreground: '00bfae' },
41
+ { token: 'function', foreground: 'dcdcaa' },
42
+ { token: 'method', foreground: 'dcdcaa' },
43
+ { token: 'property', foreground: '9cdcfe' },
44
+ { token: 'function.identifier', foreground: 'dcdcaa' },
45
+ { token: 'member', foreground: 'dcdcaa' },
46
+ { token: 'constant', foreground: '4fc1ff' },
47
+ { token: 'constant.numeric', foreground: 'b5cea8' },
48
+ { token: 'constant.language', foreground: '569cd6' },
49
+ { token: 'tag', foreground: '569cd6' },
50
+ { token: 'tag.xml', foreground: '569cd6' },
51
+ { token: 'metatag', foreground: '808080' },
52
+ { token: 'attribute.name', foreground: '9cdcfe' },
53
+ { token: 'attribute.value', foreground: 'ce9178' },
54
+ { token: 'delimiter', foreground: 'd4d4d4' },
55
+ { token: 'delimiter.bracket', foreground: 'ffd700' },
56
+ { token: 'delimiter.html', foreground: '808080' },
57
+ { token: 'operator', foreground: 'd4d4d4' },
58
+ { token: 'namespace', foreground: '4ec9b0' },
59
+ { token: 'class', foreground: '4ec9b0' },
60
+ { token: 'interface', foreground: '4ec9b0' },
61
+ { token: 'enum', foreground: '4ec9b0' },
62
+ { token: 'struct', foreground: '4ec9b0' },
63
+ { token: 'annotation', foreground: 'dcdcaa' },
64
+ { token: 'punctuation', foreground: 'd4d4d4' },
65
+ { token: 'invalid', foreground: 'f44747' },
66
+ ]
67
+
68
+ const LIGHT_RULES = [
69
+ { token: '', foreground: '1e1e1e', background: 'ffffff' },
70
+ { token: 'comment', foreground: '008000', fontStyle: 'italic' },
71
+ { token: 'comment.doc', foreground: '2e7d32', fontStyle: 'italic' },
72
+ { token: 'keyword', foreground: '0000ff' },
73
+ { token: 'keyword.control', foreground: 'af00db' },
74
+ { token: 'keyword.operator', foreground: '1e1e1e' },
75
+ { token: 'keyword.json', foreground: '0451a5' },
76
+ { token: 'string', foreground: 'a31515' },
77
+ { token: 'string.escape', foreground: 'ee0000' },
78
+ { token: 'string.key.json', foreground: '0451a5' },
79
+ { token: 'string.value.json', foreground: 'a31515' },
80
+ { token: 'string.yaml', foreground: 'a31515' },
81
+ { token: 'number', foreground: '098658' },
82
+ { token: 'number.hex', foreground: '098658' },
83
+ { token: 'regexp', foreground: '811f3f' },
84
+ { token: 'type', foreground: '267f99' },
85
+ { token: 'type.identifier', foreground: '267f99' },
86
+ { token: 'identifier', foreground: '1e1e1e' },
87
+ { token: 'variable', foreground: '001080' },
88
+ { token: 'variable.global', foreground: 'c2185b' },
89
+ { token: 'variable.local', foreground: '001080' },
90
+ { token: 'variable.mutable', foreground: '001080' },
91
+ { token: 'variable.predefined', foreground: '001080' },
92
+ { token: 'variable.parameter', foreground: '008577' },
93
+ { token: 'parameter', foreground: '008577' },
94
+ { token: 'function', foreground: '795e26' },
95
+ { token: 'method', foreground: '795e26' },
96
+ { token: 'function.identifier', foreground: '795e26' },
97
+ { token: 'member', foreground: '795e26' },
98
+ { token: 'property', foreground: '001080' },
99
+ { token: 'constant', foreground: '0070c1' },
100
+ { token: 'constant.numeric', foreground: '098658' },
101
+ { token: 'constant.language', foreground: '0000ff' },
102
+ { token: 'tag', foreground: '800000' },
103
+ { token: 'tag.xml', foreground: '800000' },
104
+ { token: 'metatag', foreground: '6b6b6b' },
105
+ { token: 'attribute.name', foreground: 'e50000' },
106
+ { token: 'attribute.value', foreground: '0451a5' },
107
+ { token: 'delimiter', foreground: '1e1e1e' },
108
+ { token: 'delimiter.bracket', foreground: 'b46400' },
109
+ { token: 'delimiter.html', foreground: '800000' },
110
+ { token: 'operator', foreground: '1e1e1e' },
111
+ { token: 'namespace', foreground: '267f99' },
112
+ { token: 'class', foreground: '267f99' },
113
+ { token: 'interface', foreground: '267f99' },
114
+ { token: 'enum', foreground: '267f99' },
115
+ { token: 'struct', foreground: '267f99' },
116
+ { token: 'annotation', foreground: '795e26' },
117
+ { token: 'punctuation', foreground: '1e1e1e' },
118
+ { token: 'invalid', foreground: 'cd3131' },
119
+ ]
120
+
121
+ /** 编辑器 UI 配色(与语法配色同套,保证 gutter/minimap/selection 一致)。 */
122
+ const DARK_COLORS = {
123
+ 'editor.background': '#1e1e1e',
124
+ 'editor.foreground': '#d4d4d4',
125
+ 'editorLineNumber.foreground': '#6e7681',
126
+ 'editorLineNumber.activeForeground': '#c6c6c6',
127
+ 'editor.selectionBackground': '#264f78',
128
+ 'editor.inactiveSelectionBackground': '#3a3d41',
129
+ 'editor.lineHighlightBackground': '#2a2d2e',
130
+ 'editorCursor.foreground': '#aeafad',
131
+ 'editorIndentGuide.background1': '#404040',
132
+ 'editorIndentGuide.activeBackground1': '#707070',
133
+ 'editorWidget.background': '#252526',
134
+ 'editorHoverWidget.background': '#252526',
135
+ 'editorGutter.background': '#1e1e1e',
136
+ 'minimap.background': '#1e1e1e',
137
+ 'scrollbarSlider.background': '#79797966',
138
+ 'scrollbarSlider.hoverBackground': '#646464b3',
139
+ 'scrollbarSlider.activeBackground': '#bfbfbf66',
140
+ }
141
+
142
+ const LIGHT_COLORS = {
143
+ 'editor.background': '#ffffff',
144
+ 'editor.foreground': '#1e1e1e',
145
+ 'editorLineNumber.foreground': '#6c6c6c',
146
+ 'editorLineNumber.activeForeground': '#171184',
147
+ 'editor.selectionBackground': '#add6ff',
148
+ 'editor.inactiveSelectionBackground': '#e5ebf1',
149
+ 'editor.lineHighlightBackground': '#f0f0f0',
150
+ 'editorCursor.foreground': '#000000',
151
+ 'editorIndentGuide.background1': '#d3d3d3',
152
+ 'editorIndentGuide.activeBackground1': '#939393',
153
+ 'editorWidget.background': '#f3f3f3',
154
+ 'editorHoverWidget.background': '#f3f3f3',
155
+ 'editorGutter.background': '#ffffff',
156
+ 'minimap.background': '#ffffff',
157
+ 'scrollbarSlider.background': '#64646466',
158
+ 'scrollbarSlider.hoverBackground': '#646464b3',
159
+ 'scrollbarSlider.activeBackground': '#00000099',
160
+ }
161
+
162
+ /**
163
+ * 注册 edrv 双套主题(幂等;Monaco 未加载时静默跳过)。
164
+ * @author ddj 2026年08月28号
165
+ * @param monaco Monaco 实例(window.monaco)
166
+ */
167
+ export function registerThemes(monaco) {
168
+ if (!monaco?.editor?.defineTheme) return
169
+ try {
170
+ monaco.editor.defineTheme(EDRV_DARK, {
171
+ base: 'vs-dark',
172
+ inherit: true,
173
+ rules: DARK_RULES,
174
+ colors: DARK_COLORS,
175
+ })
176
+ monaco.editor.defineTheme(EDRV_LIGHT, {
177
+ base: 'vs',
178
+ inherit: true,
179
+ rules: LIGHT_RULES,
180
+ colors: LIGHT_COLORS,
181
+ })
182
+ } catch (error) {
183
+ /* 主题注册失败保留内置 vs */
184
+ }
185
+ }
186
+
187
+ /**
188
+ * 探测当前 DSH 明暗(文档根 data 主题属性 → prefers-color-scheme → 暗)。
189
+ * @author ddj 2026年08月28号
190
+ * @returns 'dark' | 'light'
191
+ */
192
+ export function detectColorScheme() {
193
+ try {
194
+ const attr = document.documentElement?.getAttribute?.('data-theme')
195
+ ?? document.body?.getAttribute?.('data-theme')
196
+ const value = String(attr ?? '').toLowerCase()
197
+ if (value.includes('light') || value.includes('catppuccin-latte')) return 'light'
198
+ if (value.includes('dark') || value.includes('frappe') || value.includes('macchiato') || value.includes('mocha')) return 'dark'
199
+ if (typeof window.matchMedia === 'function') {
200
+ return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'
201
+ }
202
+ } catch (error) {
203
+ /* 探测失败回落暗色 */
204
+ }
205
+ return 'dark'
206
+ }
207
+
208
+ /**
209
+ * 主题名:按当前明暗返回已注册主题(未注册回落 vs)。
210
+ * @author ddj 2026年08月28号
211
+ * @returns 主题 id
212
+ */
213
+ export function themeNameOf() {
214
+ return detectColorScheme() === 'light' ? EDRV_LIGHT : EDRV_DARK
215
+ }
216
+
217
+ /**
218
+ * 应用主题到 Monaco(全局切换;新建编辑器也用该主题)。
219
+ * @author ddj 2026年08月28号
220
+ * @param monaco Monaco 实例
221
+ */
222
+ export function applyTheme(monaco) {
223
+ if (!monaco?.editor?.setTheme) return
224
+ try {
225
+ monaco.editor.setTheme(themeNameOf())
226
+ } catch (error) {
227
+ /* setTheme 失败忽略 */
228
+ }
229
+ }
@@ -9,6 +9,7 @@ import React from 'react'
9
9
  import { dbg, rpc } from '../rpc.js'
10
10
  import { emitRefresh } from '../events.js'
11
11
  import { langOf, loadMonaco } from '../monaco/loader.js'
12
+ import { applyTheme, registerThemes, themeNameOf } from '../monaco/theme.js'
12
13
  import { createDiffRenderer } from '../monaco/diffRender.js'
13
14
  import { ST, callIdAttr, noopHunk, summarize } from '../state/records.js'
14
15
  import { diffRegions } from '../state/regions.js'
@@ -244,7 +245,9 @@ export function EditorView(props) {
244
245
  setLoadStage((prev) => ({ progress: Math.max(84, prev.progress), message: '文件已读取,准备创建编辑器…' }))
245
246
  setStatus('已加载')
246
247
  } else {
247
- const message = res?.error ? String(res.error) : '读取失败'
248
+ const base = res?.error ? String(res.error) : '读取失败'
249
+ // 带出 host 解析后的真实路径:跳转失败时一眼看出是路径解析错还是目标不存在
250
+ const message = res?.resolvedPath ? base + ':' + String(res.resolvedPath) : base
248
251
  setLoadError(message)
249
252
  setError(message)
250
253
  setStatus('读取失败')
@@ -496,6 +499,22 @@ export function EditorView(props) {
496
499
  return () => { alive = false }
497
500
  }, [monaco, monacoErr])
498
501
 
502
+ // 分色主题:Monaco 就绪后注册(幂等)+ 应用;DSH 明暗切换时跟随重刷。
503
+ React.useEffect(() => {
504
+ if (!monaco) return
505
+ registerThemes(monaco)
506
+ applyTheme(monaco)
507
+ if (typeof window.matchMedia !== 'function') return
508
+ const query = window.matchMedia('(prefers-color-scheme: dark)')
509
+ const onChange = () => applyTheme(monaco)
510
+ if (typeof query.addEventListener === 'function') {
511
+ query.addEventListener('change', onChange)
512
+ return () => query.removeEventListener('change', onChange)
513
+ }
514
+ query.addListener(onChange)
515
+ return () => query.removeListener(onChange)
516
+ }, [monaco])
517
+
499
518
  monacoRef.current = monaco
500
519
 
501
520
  const getModel = (path, text) => {
@@ -587,7 +606,7 @@ export function EditorView(props) {
587
606
  const ed = m.editor.create(node, {
588
607
  value: '',
589
608
  language: 'plaintext',
590
- theme: 'vs',
609
+ theme: themeNameOf(),
591
610
  fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)',
592
611
  fontSize: 13,
593
612
  lineHeight: 20,
@@ -10,7 +10,7 @@ import { rpc } from '../rpc.js'
10
10
  import '../styles/mcp.css'
11
11
 
12
12
  const LANGUAGES = [
13
- { id: 'lua', label: 'Lua(LuaLS)', hint: 'lua-language-server,EmmyLua 注解原生支持' },
13
+ { id: 'lua', label: 'Lua(EmmyLua / LuaLS)', hint: '优先使用已安装 EmmyLua;未安装时回退 LuaLS 或 PATH 中的 lua-language-server' },
14
14
  { id: 'csharp', label: 'C#(Roslyn / OmniSharp)', hint: '需 dotnet + ms-dotnettools.csharp 的 .roslyn 服务器,或手动指定' },
15
15
  ]
16
16
 
@@ -25,8 +25,8 @@ const TABS = [
25
25
  { id: 'updates', label: '更新' },
26
26
  ]
27
27
 
28
- /** 语言卡片:状态 + 启用开关 + 命令/路径配置。 */
29
- function LangCard({ lang, config, status, busy, onToggle, onSave }) {
28
+ /** 语言卡片:状态 + 启用开关 + 命令/路径配置 + 重新检测。 */
29
+ function LangCard({ lang, config, status, busy, onToggle, onSave, onRedetect }) {
30
30
  const [pathDraft, setPathDraft] = React.useState(config?.path ?? '')
31
31
  const [commandDraft, setCommandDraft] = React.useState(config?.command ?? '')
32
32
  React.useEffect(() => { setPathDraft(config?.path ?? ''); setCommandDraft(config?.command ?? '') }, [config?.path, config?.command])
@@ -40,12 +40,15 @@ function LangCard({ lang, config, status, busy, onToggle, onSave }) {
40
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
41
  React.createElement('div', { className: 'vsm-mcp-meta' },
42
42
  PHASE_LABEL[phase] ?? phase, ' · ', SOURCE_LABEL[status?.source] ?? status?.source,
43
+ status?.providerName ? ' · ' + status.providerName : '',
44
+ status?.version ? ' · v' + status.version : '',
43
45
  (phase === 'ready' && status?.root) ? ' · ' + String(status.root).split(/[\\/]/).pop() : ''),
44
46
  status?.reason ? React.createElement('div', { className: 'vsm-mcp-error' }, status.reason) : null,
45
47
  React.createElement('div', { className: 'vsm-lsp-form' },
46
48
  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
49
  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 ? '保存中…' : '保存配置')),
50
+ React.createElement('button', { className: 'vsm-primary', disabled: busy === lang.id, onClick: () => onSave(lang.id, pathDraft, commandDraft) }, busy === lang.id ? '保存中…' : '保存配置'),
51
+ React.createElement('button', { className: 'vsm-primary vsm-small', disabled: busy === lang.id, onClick: () => onRedetect(lang.id), title: '清缓存并重新选择语言服务器(EmmyLua / LuaLS)' }, busy === lang.id ? '检测中…' : '重新检测')),
49
52
  React.createElement('div', { className: 'vsm-lsp-hint' }, lang.hint))
50
53
  }
51
54
 
@@ -129,6 +132,18 @@ export function LspSettings() {
129
132
  if (next === 'servers') void refreshServers()
130
133
  }
131
134
 
135
+ /** 合并 redetect/configUpdate 返回的服务器状态:只替换该语言条目。 */
136
+ const mergeServers = (languageId, list) => {
137
+ setServers((prev) => Array.isArray(list)
138
+ ? [...prev.filter((s) => s.languageId !== languageId), ...list]
139
+ : prev)
140
+ }
141
+
142
+ /** 重检测后通知编辑器:与 host 重新同步已打开的模型(触发重新 acquire)。 */
143
+ const notifyResync = (languageId) => {
144
+ window.dispatchEvent(new CustomEvent('edrv:lsp-redetect', { detail: { languageId } }))
145
+ }
146
+
132
147
  const saveLang = (languageId, path, command) => {
133
148
  setBusy(languageId)
134
149
  setError('')
@@ -136,6 +151,8 @@ export function LspSettings() {
136
151
  .then((res) => {
137
152
  if (!res?.ok) { setError(res?.error ?? '保存失败'); return }
138
153
  setConfig(res.config ?? {})
154
+ mergeServers(languageId, res.servers)
155
+ notifyResync(languageId) // 保存后自动重新检测 + 同步模型
139
156
  })
140
157
  .catch((e) => setError(String(e)))
141
158
  .finally(() => setBusy(''))
@@ -145,8 +162,23 @@ export function LspSettings() {
145
162
  setBusy(languageId)
146
163
  rpc('edrv.lsp.configUpdate', { languageId, enabled })
147
164
  .then((res) => {
148
- if (res?.ok) { setConfig(res.config ?? {}); void refreshServers() }
149
- else setError(res?.error ?? '切换失败')
165
+ if (!res?.ok) { setError(res?.error ?? '切换失败'); return }
166
+ setConfig(res.config ?? {})
167
+ mergeServers(languageId, res.servers)
168
+ notifyResync(languageId) // 切换启用状态后自动重新检测 + 同步模型
169
+ })
170
+ .catch((e) => setError(String(e)))
171
+ .finally(() => setBusy(''))
172
+ }
173
+
174
+ const redetectLang = (languageId) => {
175
+ setBusy(languageId)
176
+ setError('')
177
+ rpc('edrv.lsp.redetect', { languageId })
178
+ .then((res) => {
179
+ if (!res?.ok) { setError(res?.error ?? '重新检测失败'); return }
180
+ mergeServers(languageId, res.servers)
181
+ notifyResync(languageId)
150
182
  })
151
183
  .catch((e) => setError(String(e)))
152
184
  .finally(() => setBusy(''))
@@ -239,7 +271,7 @@ export function LspSettings() {
239
271
  key: lang.id, lang,
240
272
  config: config[lang.id] ?? {},
241
273
  status: servers.find((s) => s.languageId === lang.id),
242
- busy, onToggle: toggleLang, onSave: saveLang,
274
+ busy, onToggle: toggleLang, onSave: saveLang, onRedetect: redetectLang,
243
275
  })),
244
276
  React.createElement('div', { className: 'vsm-lsp-hint' }, '提示:打开 Lua/C# 文件即自动(惰性)启动对应语言服务器;未配置时编辑器功能不受影响,大纲回退内置解析。')))
245
277
  } else if (tab === 'installed') {
package/src/lsp/client.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * 作者 ddj 2026-08-27
6
6
  */
7
7
  import { createFrameParser, encodeMessage, requestMessage, notifyMessage } from './jsonrpc.js'
8
- import type { LspServerCapabilities } from '../shared/lsp.js'
8
+ import { LSP_SEMANTIC_TOKEN_TYPES, LSP_SEMANTIC_TOKEN_MODIFIERS, type LspServerCapabilities } from '../shared/lsp.js'
9
9
 
10
10
  export interface LspClientEvents {
11
11
  onReady?: (capabilities: LspServerCapabilities) => void
@@ -54,7 +54,16 @@ export function createLspClient(
54
54
  let nextId = 1
55
55
  let alive = transport.alive
56
56
  const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }>()
57
- let serverCapabilities: LspServerCapabilities = { definition: false, references: false, documentSymbol: false, workspaceSymbol: false, hover: false }
57
+ let serverCapabilities: LspServerCapabilities = {
58
+ definition: false,
59
+ references: false,
60
+ documentSymbol: false,
61
+ workspaceSymbol: false,
62
+ hover: false,
63
+ semanticTokens: false,
64
+ semanticTokenTypes: [],
65
+ semanticTokenModifiers: [],
66
+ }
58
67
 
59
68
  const log = (line: string): void => events.onLog?.(line)
60
69
  const parser = createFrameParser()
@@ -153,6 +162,9 @@ export function createLspClient(
153
162
  documentSymbolProvider?: unknown
154
163
  workspaceSymbolProvider?: unknown
155
164
  hoverProvider?: unknown
165
+ semanticTokensProvider?: {
166
+ legend?: { tokenTypes?: unknown; tokenModifiers?: unknown }
167
+ }
156
168
  }
157
169
  }>('initialize', {
158
170
  processId: process.pid,
@@ -170,16 +182,31 @@ export function createLspClient(
170
182
  references: { dynamicRegistration: false },
171
183
  documentSymbol: { dynamicRegistration: false },
172
184
  hover: { dynamicRegistration: false },
185
+ semanticTokens: {
186
+ dynamicRegistration: false,
187
+ requests: { range: false, full: { delta: false } },
188
+ tokenTypes: [...LSP_SEMANTIC_TOKEN_TYPES],
189
+ tokenModifiers: [...LSP_SEMANTIC_TOKEN_MODIFIERS],
190
+ formats: ['relative'],
191
+ },
173
192
  },
174
193
  },
175
194
  })
176
195
  const caps = init?.capabilities ?? {}
196
+ const legend = caps.semanticTokensProvider?.legend
177
197
  serverCapabilities = {
178
198
  definition: Boolean(caps.definitionProvider),
179
199
  references: Boolean(caps.referencesProvider),
180
200
  documentSymbol: Boolean(caps.documentSymbolProvider),
181
201
  workspaceSymbol: Boolean(caps.workspaceSymbolProvider),
182
202
  hover: Boolean(caps.hoverProvider),
203
+ semanticTokens: Boolean(caps.semanticTokensProvider),
204
+ semanticTokenTypes: Array.isArray(legend?.tokenTypes)
205
+ ? legend.tokenTypes.filter((item): item is string => typeof item === 'string')
206
+ : [...LSP_SEMANTIC_TOKEN_TYPES],
207
+ semanticTokenModifiers: Array.isArray(legend?.tokenModifiers)
208
+ ? legend.tokenModifiers.filter((item): item is string => typeof item === 'string')
209
+ : [...LSP_SEMANTIC_TOKEN_MODIFIERS],
183
210
  }
184
211
  client.notify('initialized', {})
185
212
  events.onReady?.(serverCapabilities)