dsh-vscode-mode 0.1.35 → 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.35",
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
+ }
@@ -22,6 +22,8 @@ export const CACHE_KEY = {
22
22
  search: 'edrv.search.v1.',
23
23
  /** 侧边栏提示已关闭标记(全局,无会话)。 */
24
24
  sideHint: 'edrv.side-hint-dismissed',
25
+ /** 「性能优化」页工作区栏目展开状态(workspaceFoldCache v1,全局不按会话)。 */
26
+ workspaceFold: 'edrv.ws-fold.v1.',
25
27
  } as const
26
28
 
27
29
  /** 按前缀拼会话键(侧边栏/编辑器等需要布局段时自行拼接后传入)。 */
@@ -0,0 +1,95 @@
1
+ /**
2
+ * dsh-vscode-mode client — 「性能优化」页工作区栏目展开状态持久化(纯函数,可单测)。
3
+ * 只持久化「已展开」的工作区键集合(默认全部折叠),对齐 explorerCache 的做法:
4
+ * 不缓存会话条目本身,条目由 edrv.perf.inventory 实时返回。
5
+ * 键前缀统一走 paths.ts PathConst(CACHE_KEY.workspaceFold)。
6
+ * 作者 ddj 2026-09-02
7
+ */
8
+ import { CACHE_KEY } from '../paths.js'
9
+
10
+ /** localStorage 键(全局,不按会话隔离)。 */
11
+ const STORE_KEY = CACHE_KEY.workspaceFold
12
+
13
+ /** 展开键容量上限:超出丢弃尾部,防止无限增长撑爆配额。 */
14
+ export const FOLD_CAP = 300
15
+
16
+ /**
17
+ * 归一化工作区键:仅去首尾空白(工作区键非文件系统路径,不做斜杠转换)。
18
+ * @author ddj 2026年09月02号
19
+ * @param key 工作区标识
20
+ * @returns 归一化键,空值返回 ''
21
+ */
22
+ export function normalizeFoldKey(key: string | undefined | null): string {
23
+ return String(key ?? '').trim()
24
+ }
25
+
26
+ /**
27
+ * 裁剪展开键集:去重、丢弃空值、超容量丢弃尾部(保持传入顺序)。
28
+ * @author ddj 2026年09月02号
29
+ * @param expanded 原始展开键集合
30
+ * @returns 规范化后的键数组
31
+ */
32
+ export function sanitizeFolded(expanded: unknown): string[] {
33
+ if (!Array.isArray(expanded)) return []
34
+ const seen = new Set<string>()
35
+ const out: string[] = []
36
+ for (const item of expanded) {
37
+ const key = normalizeFoldKey(typeof item === 'string' ? item : '')
38
+ if (!key || seen.has(key)) continue
39
+ seen.add(key)
40
+ out.push(key)
41
+ if (out.length >= FOLD_CAP) break
42
+ }
43
+ return out
44
+ }
45
+
46
+ /**
47
+ * 序列化展开状态(JSON,带 schema 版本号)。
48
+ * @author ddj 2026年09月02号
49
+ * @param expanded 已展开的项目键集合
50
+ * @returns JSON 字符串
51
+ */
52
+ export function serializeFold(expanded: string[]): string {
53
+ return JSON.stringify({ v: 1, expanded: sanitizeFolded(expanded) })
54
+ }
55
+
56
+ /**
57
+ * 解析缓存文本;损坏/格式不符 → 空集合。
58
+ * @author ddj 2026年09月02号
59
+ * @param text localStorage 原文
60
+ * @returns 已展开键集合
61
+ */
62
+ export function parseFold(text: string | null): string[] {
63
+ if (!text) return []
64
+ try {
65
+ const data = JSON.parse(text)
66
+ if (!data || typeof data !== 'object') return []
67
+ return sanitizeFolded(data.expanded)
68
+ } catch (error) {
69
+ return []
70
+ }
71
+ }
72
+
73
+ /**
74
+ * 读取展开状态(localStorage 不可用 → 空集合)。
75
+ * @author ddj 2026年09月02号
76
+ * @returns 已展开键集合
77
+ */
78
+ export function foldLoad(): string[] {
79
+ try {
80
+ return parseFold(window.localStorage.getItem(STORE_KEY))
81
+ } catch (error) {
82
+ return []
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 写入展开状态(配额满/隐私模式失败静默)。
88
+ * @author ddj 2026年09月02号
89
+ * @param expanded 已展开键集合
90
+ */
91
+ export function foldSave(expanded: string[]): void {
92
+ try {
93
+ window.localStorage.setItem(STORE_KEY, serializeFold(expanded))
94
+ } catch (error) { /* 配额满/隐私模式忽略 */ }
95
+ }
@@ -10,18 +10,36 @@
10
10
  .vsm-primary:hover{background:var(--dsw-alias-button-primary-hover,#5a46e6)}
11
11
  .vsm-primary:disabled{opacity:.55;cursor:default}
12
12
 
13
- /* 子 Tab:我的 MCP / MCP 市场 */
13
+ /* MCP 管理下的二级子页签:我的 MCP / 项目 MCP / MCP 市场 */
14
+ .vsm-mcp-subhead{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-top:-4px}
15
+ .vsm-mcp-subtabs{display:flex;gap:18px;border-bottom:1px solid var(--dsw-alias-border-l1,#2e2e2e);flex:1}
16
+ .vsm-mcp-subtabs button{border:0;background:none;color:var(--dsw-alias-label-tertiary,#9aa5b1);padding:7px 2px 9px;cursor:pointer;font-size:13px;position:relative}
17
+ .vsm-mcp-subtabs button:hover{color:var(--dsw-alias-label-secondary,#c8c8c8)}
18
+ .vsm-mcp-subtabs button.active{color:var(--dsw-alias-label-primary,#fff);font-weight:500}
19
+ .vsm-mcp-subtabs button.active::after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--dsw-alias-brand-primary,#6d55ff);border-radius:2px}
20
+
21
+ /* 顶层 Tab:通用 / 快捷键 / MCP 管理 / 语言服务器 / 性能优化 / 兼容性 */
14
22
  .vsm-mcp-tabs{display:flex;gap:24px;border-bottom:1px solid var(--dsw-alias-border-l2,#3a3a3a)}
15
23
  .vsm-mcp-tabs button{border:0;background:none;color:var(--dsw-alias-label-tertiary,#9aa5b1);padding:9px 2px 11px;cursor:pointer;font-size:14px;position:relative}
16
24
  .vsm-mcp-tabs button:hover{color:var(--dsw-alias-label-secondary,#c8c8c8)}
17
25
  .vsm-mcp-tabs button.active{color:var(--dsw-alias-label-primary,#fff);font-weight:500}
18
26
  .vsm-mcp-tabs button.active::after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--dsw-alias-brand-primary,#6d55ff);border-radius:2px}
19
27
 
28
+ /* 统一线框面板:圆角矩形边框圈住各设置分区,提升可读性。
29
+ 用法:<section class="vsm-panel"> + 可选 <h3 class="vsm-panel-title">。 */
30
+ .vsm-panel{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:12px;background:var(--dsw-alias-bg-layer-2,transparent);overflow:hidden}
31
+ .vsm-panel-pad{padding:14px 16px}
32
+ .vsm-panel-title{margin:0;padding:10px 16px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary,#c8c8c8);background:var(--dsw-alias-bg-mask-1,#ffffff0a);border-bottom:1px solid var(--dsw-alias-border-l1,#333)}
33
+ .vsm-panel-body{padding:6px 16px}
34
+ .vsm-panel-body>*:first-child{margin-top:8px}
35
+ .vsm-panel-body>*:last-child{margin-bottom:10px}
36
+
20
37
  /* 通用设置 */
21
- .vsm-general-page{display:flex;flex-direction:column;gap:10px;max-width:720px}
38
+ .vsm-general-page{display:flex;flex-direction:column;gap:14px;max-width:720px}
22
39
  .vsm-general-page h2{margin:0;font-size:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}
23
- .vsm-general-page p{margin:0 0 8px;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:13px}
40
+ .vsm-general-page p{margin:0;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:13px}
24
41
  .vsm-general-row{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:14px 0;border-bottom:1px solid var(--dsw-alias-border-l1,#333);color:var(--dsw-alias-label-primary,#eee);font-size:14px}
42
+ .vsm-general-row:last-child{border-bottom:0}
25
43
  .vsm-general-row select{min-width:220px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:var(--dsw-alias-bg-layer-1,#2b2b2b);color:var(--dsw-alias-label-primary,#eee);font:inherit}
26
44
  .vsm-general-row small{color:var(--dsw-alias-label-tertiary,#9aa5b1)}
27
45
 
@@ -94,8 +112,7 @@
94
112
  .vsm-mcp-dialog-actions button{padding:8px 14px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:none;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;font-size:13px}
95
113
  .vsm-mcp-dialog-actions button:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
96
114
 
97
- /* 兼容性小节 */
98
- .vsm-compat-h3{margin:16px 0 2px;font-size:14px;font-weight:600;color:var(--dsw-alias-label-secondary,#c8c8c8)}
115
+ /* 兼容性条目(面板标题改用 .vsm-panel-title) */
99
116
  .vsm-compat-item{display:flex;align-items:baseline;justify-content:space-between;gap:16px;padding:10px 0;border-bottom:1px solid var(--dsw-alias-border-l1,#333);font-size:13px;color:var(--dsw-alias-label-primary,#eee)}
100
117
  .vsm-compat-item.ok .vsm-compat-name::before{content:"● ";color:var(--dsw-alias-state-success-primary,#38bd75)}
101
118
  .vsm-compat-item.warn .vsm-compat-name::before{content:"● ";color:var(--dsw-alias-state-warn-primary,#e5b93f)}
@@ -139,6 +156,7 @@
139
156
 
140
157
  /* 会话性能(PerfSettings 子 Tab) */
141
158
  .vsm-perf-page{display:flex;flex-direction:column;gap:14px;max-width:920px}
159
+ .vsm-perf-page>.vsm-panel-body>*:first-child{margin-top:0}
142
160
  .vsm-perf-page h2{margin:0;font-size:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}
143
161
  .vsm-perf-page p{margin:0;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:13px;line-height:1.5}
144
162
  .vsm-perf-cards{display:flex;gap:12px;flex-wrap:wrap}
@@ -148,8 +166,19 @@
148
166
  .vsm-perf-card b.big{color:var(--dsw-alias-state-error-primary,#e05252)}
149
167
  .vsm-perf-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
150
168
  .vsm-perf-selcount{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);margin:0 4px}
169
+ /* 工作区列表分页器 */
170
+ .vsm-perf-pager{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-top:4px}
171
+ .vsm-perf-pager-text{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1)}
172
+ .vsm-perf-pager-actions{display:flex;gap:8px}
173
+ .vsm-perf-ws-list{display:flex;flex-direction:column;gap:8px}
151
174
  .vsm-perf-ws{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;overflow:hidden}
152
- .vsm-perf-ws-head{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:10px 14px;background:var(--dsw-alias-bg-layer-1,#262626);border-bottom:1px solid var(--dsw-alias-border-l2,#3a3a3a)}
175
+ .vsm-perf-ws-head{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--dsw-alias-bg-layer-1,#262626);cursor:pointer;user-select:none}
176
+ .vsm-perf-ws-head:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
177
+ .vsm-perf-ws-head:focus-visible{outline:1px solid var(--dsw-alias-brand-primary,#6d55ff);outline-offset:-1px}
178
+ .vsm-perf-ws.expanded .vsm-perf-ws-head{border-bottom:1px solid var(--dsw-alias-border-l2,#3a3a3a)}
179
+ .vsm-perf-ws-caret{color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:12px;flex-shrink:0;transition:transform .15s;display:inline-block}
180
+ .vsm-perf-ws-caret.open{transform:rotate(90deg)}
181
+ .vsm-perf-ws .vsm-perf-ws-name{flex:1}
153
182
  .vsm-perf-ws-name{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6);font-family:var(--ds-font-family-code,ui-monospace,monospace);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
154
183
  .vsm-perf-ws-meta{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);flex-shrink:0}
155
184
  .vsm-perf-rows{display:flex;flex-direction:column}
@@ -163,15 +192,9 @@
163
192
  .vsm-perf-size.big{color:var(--dsw-alias-state-error-primary,#e05252);font-weight:600}
164
193
  .vsm-perf-mtime{width:44px;flex-shrink:0;color:var(--dsw-alias-label-tertiary,#9aa5b1)}
165
194
  .vsm-perf-active{width:44px;flex-shrink:0;font-size:11px;color:var(--dsw-alias-state-warn-primary,#e5b93f);text-align:right}
166
- .vsm-perf-moved{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-2,transparent)}
167
- .vsm-perf-moved h3{margin:0 0 8px;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}
168
- .vsm-perf-config{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-2,transparent)}
169
- .vsm-perf-config h3{margin:0 0 8px;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}
170
195
  .vsm-perf-config-state{display:flex;align-items:center;gap:12px;margin:10px 0}
171
196
  .vsm-perf-note{font-size:12px;color:var(--dsw-alias-state-warn-primary,#e5b93f)}
172
197
  .vsm-perf-block{margin:8px 0 0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:8px;background:var(--dsw-alias-bg-base,#111);color:var(--dsw-alias-label-secondary,#c8c8c8);font-size:11px;line-height:1.5;font-family:var(--ds-font-family-code,ui-monospace,monospace);white-space:pre-wrap;overflow-x:auto;max-width:100%}
173
- .vsm-perf-guide{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-2,transparent)}
174
- .vsm-perf-guide h3{margin:0 0 8px;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}
175
198
  .vsm-perf-modal{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:var(--dsw-alias-bg-mask-1,#000000aa);z-index:1000;padding:20px}
176
199
  .vsm-perf-modal-card{width:min(560px,92vw);max-height:72vh;display:flex;flex-direction:column;gap:12px;padding:18px 20px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#1f1f1f);color:var(--dsw-alias-label-primary,#e6e6e6)}
177
200
  .vsm-perf-modal-card h3{margin:0;font-size:16px}