dsh-vscode-mode 0.1.62 → 0.2.0

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.
@@ -2,11 +2,15 @@
2
2
  * dsh-vscode-mode client — 快捷键配置模块(解析/匹配/状态同步)。
3
3
  * 纯逻辑(parseChord/parseChords/formatChord/matchEvent/chordFromEvent/normalizeKey)不依赖 DOM,可单测;
4
4
  * 模块状态由 settings 订阅驱动(client/index.ts 调 keybindingsApply)。
5
- * 键位语义:Ctrl 与 Cmd 互认(延续现有 Ctrl+P/Ctrl+B 捕获行为)。
6
- * 作者 ddj 2026年08月26号
5
+ * 键位语义:Ctrl 与 Cmd 互认(延续 Ctrl+P/Ctrl+B 捕获行为)。
6
+ * 命令目录(COMMANDS)派生自指令目录 commandCatalog(指令 = 单一数据源,设置页自动跟随);
7
+ * 另支持运行时键位表(第三方命令 register 时声明,不落设置 schema,注销即失效)。
8
+ * 作者 ddj 2026年08月26号 / 2026年09月10号
7
9
  */
8
10
  import React from 'react'
9
11
  import { KEYBINDING_DEFAULTS, normalizeKeybindings } from '../shared/keybindings.js'
12
+ import { BRIDGE_COMMANDS, EDITOR_COMMANDS, showCommandsDef } from './ui/commandCatalog.js'
13
+ import { log } from './log.js'
10
14
 
11
15
  /** 解析后的键位(修饰符 + 规范化主键)。 */
12
16
  export interface Binding {
@@ -19,22 +23,39 @@ export interface Binding {
19
23
 
20
24
  /** 命令目录(设置页展示标签;执行按目录序先匹配先执行,冲突键位确定性)。 */
21
25
  export const COMMANDS: Array<{ id: string; label: string }> = [
22
- { id: 'edrv.save', label: '保存文件' },
23
- { id: 'edrv.quickOpen', label: '快速打开文件' },
24
- { id: 'edrv.toggleSidebar', label: '切换侧边栏' },
25
- { id: 'edrv.searchInFiles', label: '在工作区中搜索' },
26
- { id: 'edrv.navigateBack', label: '后退(导航历史)' },
27
- { id: 'edrv.navigateForward', label: '前进(导航历史)' },
26
+ ...EDITOR_COMMANDS.map((command) => ({ id: command.id, label: command.label })),
27
+ { id: 'edrv.showCommands', label: '显示所有命令' },
28
+ ...BRIDGE_COMMANDS.map((command) => ({ id: command.id, label: command.label })),
28
29
  ]
29
30
 
31
+ /** 命令栏命令定义(键位自检与目录展示共用;run 不参与键位逻辑)。 */
32
+ function paletteCommand(): { id: string; label: string; keybinding?: string } {
33
+ return showCommandsDef(() => {})
34
+ }
35
+
36
+ /**
37
+ * 目录与默认键位表一致性自检(仅告警不中断):新指令漏写共享表时第一时间可见。
38
+ * @author ddj 2026年09月10号
39
+ */
40
+ function checkDefaultsDrift(): void {
41
+ for (const command of [...EDITOR_COMMANDS, paletteCommand()]) {
42
+ if (!command.keybinding) continue
43
+ if (KEYBINDING_DEFAULTS[command.id] === command.keybinding) continue
44
+ log.warn('指令默认键位与共享表不一致:' + command.id
45
+ + ' 目录=' + command.keybinding + ' 共享表=' + String(KEYBINDING_DEFAULTS[command.id]))
46
+ }
47
+ }
48
+
30
49
  const MODIFIERS: Record<string, 'ctrl' | 'shift' | 'alt' | 'meta'> = {
31
50
  ctrl: 'ctrl', cmd: 'meta', meta: 'meta', shift: 'shift', alt: 'alt',
32
51
  }
33
52
 
34
53
  let current: Record<string, string> = { ...KEYBINDING_DEFAULTS }
35
- let parsed: Record<string, Binding[]> = {}
54
+ const runtime = new Map<string, Binding[]>()
36
55
  const listeners = new Set<() => void>()
37
56
 
57
+ checkDefaultsDrift()
58
+
38
59
  /**
39
60
  * 应用设置快照(与默认值合并;未知 id 丢弃;空对象 = 全部默认)。
40
61
  * 每个命令可含多候选键位(`|` 分隔),任一命中即触发。
@@ -43,8 +64,6 @@ const listeners = new Set<() => void>()
43
64
  */
44
65
  export function keybindingsApply(raw: unknown): void {
45
66
  current = { ...KEYBINDING_DEFAULTS, ...normalizeKeybindings(raw) }
46
- parsed = {}
47
- for (const id of Object.keys(current)) parsed[id] = parseChords(current[id])
48
67
  for (const listener of listeners) {
49
68
  try { listener() } catch { /* 监听器异常不影响其他订阅 */ }
50
69
  }
@@ -62,24 +81,61 @@ export function subscribeKeybindings(listener: () => void): () => void {
62
81
  }
63
82
 
64
83
  /**
65
- * 当前键位弦(未绑定/空 → null)。
66
- * @author ddj 2026年0826
84
+ * 注册运行时键位(第三方命令;优先于设置值,注销即失效)。
85
+ * @author ddj 2026年0910
86
+ * @param id 命令 id
87
+ * @param chord 键位弦(空/非法按未绑定处理)
88
+ * @returns 注销函数(幂等)
89
+ */
90
+ export function addRuntimeKeybinding(id: string, chord: string): () => void {
91
+ runtime.set(id, parseChords(chord))
92
+ notifyKeybindings()
93
+ return () => removeRuntimeKeybinding(id)
94
+ }
95
+
96
+ /**
97
+ * 移除运行时键位。
98
+ * @author ddj 2026年09月10号
99
+ * @param id 命令 id
100
+ */
101
+ export function removeRuntimeKeybinding(id: string): void {
102
+ if (!runtime.delete(id)) return
103
+ notifyKeybindings()
104
+ }
105
+
106
+ /** 通知键位订阅者(异常隔离)。 */
107
+ function notifyKeybindings(): void {
108
+ for (const listener of listeners) {
109
+ try { listener() } catch { /* 监听器异常不影响其他订阅 */ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * 当前命令的键位弦(运行时键位优先;未绑定/空 → null)。
115
+ * @author ddj 2026年08月26号 / 2026年09月10号
67
116
  * @param id 命令 id
68
117
  * @returns 键位弦或 null
69
118
  */
70
119
  export function chordOf(id: string): string | null {
120
+ if (runtime.has(id)) {
121
+ const chords = runtime.get(id) ?? []
122
+ return chords.length ? chords.map(formatChord).join('|') : null
123
+ }
71
124
  const chord = current[id]
72
125
  return typeof chord === 'string' && chord.trim() !== '' ? chord : null
73
126
  }
74
127
 
75
128
  /**
76
129
  * 当前命令的解析键位集合(未绑定/非法 → 空数组;含多候选)。
77
- * @author ddj 2026年08月26号
130
+ * @author ddj 2026年08月26号 / 2026年09月10号
78
131
  * @param id 命令 id
79
132
  * @returns 解析键位数组(可能为空)
80
133
  */
81
134
  export function bindingsOf(id: string): Binding[] {
82
- return parsed[id] ?? []
135
+ const override = runtime.get(id)
136
+ if (override) return override
137
+ const chord = current[id]
138
+ return typeof chord === 'string' ? parseChords(chord) : []
83
139
  }
84
140
 
85
141
  /**
@@ -4,7 +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
+ import { applyOfficial, registerThemes } from './theme.js'
8
8
 
9
9
  export const MONACO_BASE = '/edrv/vendor/monaco/vs'
10
10
  let monacoPromise = null
@@ -102,10 +102,10 @@ export function loadMonaco(onProgress) {
102
102
  publishStage('core', MONACO_STAGES.core.progress, MONACO_STAGES.core.message)
103
103
  window.require(['vs/editor/editor.main'], () => {
104
104
  publishStage('ready', MONACO_STAGES.ready.progress, MONACO_STAGES.ready.message)
105
- // 分色主题注册 + 应用(rich token 配色,替掉内置基础 vs 的少层次着色)
105
+ // 主题:注册现役双套(幂等)+ 应用跟随官方的令牌主题(令牌缺失时回落现役)
106
106
  try {
107
107
  registerThemes(window.monaco)
108
- applyTheme(window.monaco)
108
+ applyOfficial(window.monaco)
109
109
  } catch (error) { /* 主题失败不阻塞编辑器 */ }
110
110
  resolve(window.monaco)
111
111
  }, (err) => fail(new Error('Monaco 模块加载失败:' + String(err))))
@@ -1,10 +1,10 @@
1
1
  // @ts-nocheck
2
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
3
+ * dsh-vscode-mode client — Monaco 主题:官方令牌跟随 + 现役双套回落。
4
+ * 优先级:官方代码/UI 令牌(--shiki-* / --dsw-alias-*,DSH 0.1.5+ 主题体系,
5
+ * 含第三方皮肤)→ edrv-dark / edrv-light 两套内置配色(旧版 DSH 或令牌缺失)。
6
+ * 明暗判定:body[data-ds-dark-theme](官方)→ data-theme(旧皮肤)→ prefers-color-scheme
7
+ * 作者 ddj 2026-08-28 / 2026-09-10
8
8
  */
9
9
 
10
10
  export const EDRV_DARK = 'edrv-dark'
@@ -185,12 +185,14 @@ export function registerThemes(monaco) {
185
185
  }
186
186
 
187
187
  /**
188
- * 探测当前 DSH 明暗(文档根 data 主题属性 → prefers-color-scheme → 暗)。
189
- * @author ddj 2026年08月28号
188
+ * 探测当前 DSH 明暗(官方 data-ds-dark-theme 旧皮肤 data-theme → prefers-color-scheme → 暗)。
189
+ * @author ddj 2026年08月28号 / 2026年09月10号
190
190
  * @returns 'dark' | 'light'
191
191
  */
192
192
  export function detectColorScheme() {
193
193
  try {
194
+ const official = document.body?.getAttribute?.('data-ds-dark-theme')
195
+ if (official !== null && official !== undefined && official !== 'false') return 'dark'
194
196
  const attr = document.documentElement?.getAttribute?.('data-theme')
195
197
  ?? document.body?.getAttribute?.('data-theme')
196
198
  const value = String(attr ?? '').toLowerCase()
@@ -227,3 +229,307 @@ export function applyTheme(monaco) {
227
229
  /* setTheme 失败忽略 */
228
230
  }
229
231
  }
232
+
233
+ // --region 官方主题跟随(--shiki-* / --dsw-alias-* 令牌)
234
+
235
+ /** 动态官方主题 id 前缀(每次应用带序号重建,保证 Monaco 感知重定义)。 */
236
+ const EDRV_OFFICIAL = 'edrv-official'
237
+
238
+ /** 官方代码配色令牌 → Monaco 规则 token 前缀(粗粒度;未覆盖的规则保留现役语义分层)。 */
239
+ const RULE_TOKEN_MAP = [
240
+ ['--shiki-token-comment', ['comment']],
241
+ ['--shiki-token-keyword', ['keyword']],
242
+ ['--shiki-token-string', ['string']],
243
+ ['--shiki-token-string-expression', ['string.escape']],
244
+ ['--shiki-token-constant', ['number', 'constant']],
245
+ ['--shiki-token-function', ['function', 'method', 'member']],
246
+ ['--shiki-token-parameter', ['parameter', 'variable.parameter']],
247
+ ['--shiki-token-punctuation', ['delimiter', 'operator', 'punctuation']],
248
+ ['--shiki-token-link', ['annotation', 'metatag']],
249
+ ]
250
+
251
+ /**
252
+ * 官方 UI 令牌 → Monaco 颜色键(每键给候选令牌,按序取首个可解析值)。
253
+ * 依据运行态实测:--dsw-alias-* 定义在 body 上;--shiki-background/-foreground 未定义,
254
+ * 故代码面底色用 markdown-code-block 近似;selection 不覆盖(沿用现役可见选框)。
255
+ */
256
+ const COLOR_TOKEN_MAP = [
257
+ [['--dsw-alias-markdown-code-block', '--dsw-alias-bg-layer-2'], ['editor.background', 'editorGutter.background', 'minimap.background']],
258
+ [['--dsw-alias-label-primary', '--shiki-foreground'], ['editor.foreground']],
259
+ [['--dsw-alias-bg-layer-1', '--dsw-alias-bg-layer-3'], ['editorWidget.background', 'editorHoverWidget.background']],
260
+ [['--dsw-alias-label-tertiary'], ['editorLineNumber.foreground']],
261
+ [['--dsw-alias-label-secondary'], ['editorLineNumber.activeForeground']],
262
+ [['--dsw-alias-interactive-bg-hover'], ['editor.lineHighlightBackground', 'editor.inactiveSelectionBackground']],
263
+ ]
264
+
265
+ /** 已应用的官方主题序号(模块级计数,保证主题 id 每次唯一)。 */
266
+ let officialSeq = 0
267
+
268
+ /**
269
+ * 读官方 CSS 变量(先 body 后文档根;未定义/空值/未解析 var() 返回 undefined)。
270
+ * 依据运行态实测:DSH 的 --dsw-alias-* 定义在 body 上,仅 --shiki-token-* 在根。
271
+ * @author ddj 2026年09月10号
272
+ * @param name CSS 变量名(含 -- 前缀)
273
+ * @returns 计算样式值,或 undefined
274
+ */
275
+ export function readCssVar(name) {
276
+ try {
277
+ if (typeof window?.getComputedStyle !== 'function') return undefined
278
+ const hosts = [document?.body, document?.documentElement]
279
+ for (const host of hosts) {
280
+ if (!host) continue
281
+ const raw = window.getComputedStyle(host).getPropertyValue(name)
282
+ const value = String(raw ?? '').trim()
283
+ if (value && !value.startsWith('var(')) return value
284
+ }
285
+ return undefined
286
+ } catch (error) {
287
+ return undefined
288
+ }
289
+ }
290
+
291
+ /**
292
+ * 数值 → 两位十六进制(0-255 夹取)。
293
+ * @author ddj 2026年09月10号
294
+ * @param value 通道值
295
+ * @returns 两位小写十六进制
296
+ */
297
+ function hex2(value) {
298
+ const text = Math.max(0, Math.min(255, Math.round(Number(value) || 0))).toString(16)
299
+ return text.length === 1 ? '0' + text : text
300
+ }
301
+
302
+ /**
303
+ * CSS 颜色归一为 Monaco 可用值(#rgb/#rrggbb/#rrggbbaa 原样,rgb()/hsl() 转 hex,其余 undefined)。
304
+ * @author ddj 2026年09月10号
305
+ * @param value CSS 计算样式颜色值
306
+ * @returns Monaco 颜色字符串,或 undefined(不可识别)
307
+ */
308
+ export function toMonacoColor(value) {
309
+ const text = String(value ?? '').trim().toLowerCase()
310
+ if (!text || text.startsWith('var(')) return undefined
311
+ if (/^#[0-9a-f]{3}$/.test(text)) return '#' + text.slice(1).split('').map((c) => c + c).join('')
312
+ if (/^#[0-9a-f]{6}$/.test(text) || /^#[0-9a-f]{8}$/.test(text)) return text
313
+ return channelColor(text)
314
+ }
315
+
316
+ /**
317
+ * rgb()/rgba()/hsl()/hsla() → Monaco 颜色(兼容逗号与空格/斜杠两种参数语法)。
318
+ * @author ddj 2026年09月10号
319
+ * @param text 小写颜色文本
320
+ * @returns Monaco 颜色,或 undefined(不可识别)
321
+ */
322
+ function channelColor(text) {
323
+ const body = /^(?:rgba?|hsla?)\(([^)]+)\)$/.exec(text)?.[1]
324
+ if (!body) return undefined
325
+ const parts = body.split(/[\s,/]+/).filter(Boolean)
326
+ if (parts.length < 3) return undefined
327
+ let channels
328
+ if (text.startsWith('hsl')) {
329
+ const h = Number(parts[0])
330
+ const s = percentOf(parts[1])
331
+ const l = percentOf(parts[2])
332
+ if (![h, s, l].every(Number.isFinite)) return undefined
333
+ channels = hslToRgb(h, s, l)
334
+ } else {
335
+ channels = parts.slice(0, 3).map(Number)
336
+ }
337
+ if (channels.some((n) => !Number.isFinite(n))) return undefined
338
+ const alpha = parts.length > 3 ? alphaOf(parts[3]) : 1
339
+ const tail = Number.isFinite(alpha) && alpha < 1 ? hex2(alpha * 255) : ''
340
+ return '#' + channels.map(hex2).join('') + tail
341
+ }
342
+
343
+ /**
344
+ * 百分数文本 → 数值('5%' → 5,'0.5' → 0.5)。
345
+ * @author ddj 2026年09月10号
346
+ * @param value 参数文本
347
+ * @returns 数值(非数字为 NaN)
348
+ */
349
+ function percentOf(value) {
350
+ const text = String(value ?? '')
351
+ return text.endsWith('%') ? Number(text.slice(0, -1)) : Number(text)
352
+ }
353
+
354
+ /**
355
+ * alpha 参数归一('5%' → 0.05,'0.5' → 0.5)。
356
+ * @author ddj 2026年09月10号
357
+ * @param value 参数文本
358
+ * @returns 0-1 的 alpha(非数字为 NaN)
359
+ */
360
+ function alphaOf(value) {
361
+ const text = String(value ?? '')
362
+ return text.endsWith('%') ? Number(text.slice(0, -1)) / 100 : Number(text)
363
+ }
364
+
365
+ /**
366
+ * HSL → RGB 通道(h 度、s/l 为 0-100 百分数)。
367
+ * @author ddj 2026年09月10号
368
+ * @param h 色相(度)
369
+ * @param s 饱和度(0-100)
370
+ * @param l 亮度(0-100)
371
+ * @returns [r, g, b](0-255)
372
+ */
373
+ function hslToRgb(h, s, l) {
374
+ const hue = ((h % 360) + 360) % 360
375
+ const sat = Math.max(0, Math.min(100, s)) / 100
376
+ const lum = Math.max(0, Math.min(100, l)) / 100
377
+ const c = (1 - Math.abs(2 * lum - 1)) * sat
378
+ const x = c * (1 - Math.abs(((hue / 60) % 2) - 1))
379
+ const m = lum - c / 2
380
+ const table = [[c, x, 0], [x, c, 0], [0, c, x], [0, x, c], [x, 0, c], [c, 0, x]]
381
+ return table[Math.min(5, Math.floor(hue / 60))].map((v) => (v + m) * 255)
382
+ }
383
+
384
+ /**
385
+ * 官方令牌 → Monaco 颜色键覆盖表(候选令牌按序取首个可解析值;全缺则不写入,保持现役回落)。
386
+ * @author ddj 2026年09月10号
387
+ * @returns 颜色键 → 颜色值
388
+ */
389
+ function tokenColors() {
390
+ const out = {}
391
+ for (const entry of COLOR_TOKEN_MAP) {
392
+ const color = firstColor(entry[0])
393
+ if (color === undefined) continue
394
+ for (const key of entry[1]) out[key] = color
395
+ }
396
+ return out
397
+ }
398
+
399
+ /**
400
+ * 候选令牌中首个可解析为 Monaco 颜色的值。
401
+ * @author ddj 2026年09月10号
402
+ * @param names CSS 变量名候选(按优先级)
403
+ * @returns Monaco 颜色,或 undefined(全部缺失/不可解析)
404
+ */
405
+ function firstColor(names) {
406
+ for (const name of names) {
407
+ const color = toMonacoColor(readCssVar(name))
408
+ if (color !== undefined) return color
409
+ }
410
+ return undefined
411
+ }
412
+
413
+ /**
414
+ * 官方令牌 → Monaco 规则前缀覆盖表(值去掉 # 前缀,Monaco rules 约定)。
415
+ * @author ddj 2026年09月10号
416
+ * @returns token 前缀 → 十六进制色(无 #)
417
+ */
418
+ function tokenRules() {
419
+ const out = {}
420
+ for (const entry of RULE_TOKEN_MAP) {
421
+ const color = toMonacoColor(readCssVar(entry[0]))
422
+ if (color === undefined) continue
423
+ for (const prefix of entry[1]) out[prefix] = color.replace('#', '')
424
+ }
425
+ return out
426
+ }
427
+
428
+ /**
429
+ * 现役规则叠加令牌覆盖(前缀命中即换色,未命中保持语义分层)。
430
+ * @author ddj 2026年09月10号
431
+ * @param baseRules 现役规则表(edrv-dark / edrv-light)
432
+ * @param overrides token 前缀 → 颜色
433
+ * @returns 覆盖后的规则表(无覆盖时原样返回)
434
+ */
435
+ function withOverrides(baseRules, overrides) {
436
+ const prefixes = Object.keys(overrides)
437
+ if (!prefixes.length) return baseRules
438
+ return baseRules.map((rule) => {
439
+ const hit = prefixes.find((p) => rule.token === p || rule.token.startsWith(p + '.'))
440
+ return hit === undefined ? rule : Object.assign({}, rule, { foreground: overrides[hit] })
441
+ })
442
+ }
443
+
444
+ /**
445
+ * 基础规则(token 为空串)前景色跟随官方 editor.foreground。
446
+ * Monaco 的 '' 规则匹配所有未被更具体规则命中的 token,不改它则内置 #1e1e1e 会盖住
447
+ * editor.foreground 令牌色(实测 .mtk1 仍为内置值的原因)。
448
+ * @author ddj 2026年09月10号
449
+ * @param rules 现役规则表
450
+ * @param color 已解析的 editor.foreground(含 #,缺省则不覆盖)
451
+ * @returns 基础规则换色后的规则表
452
+ */
453
+ export function withBaseForeground(rules, color) {
454
+ if (!color) return rules
455
+ const foreground = String(color).replace('#', '')
456
+ return rules.map((rule) => (rule.token === '' ? Object.assign({}, rule, { foreground }) : rule))
457
+ }
458
+
459
+ /**
460
+ * 构建跟随官方的 Monaco 主题(令牌全缺时 hasTokens=false,调用方回落现役双套)。
461
+ * @author ddj 2026年09月10号
462
+ * @param scheme 明暗('light' 之外一律按暗色)
463
+ * @returns 主题定义(base/rules/colors/hasTokens)
464
+ */
465
+ export function officialThemeOf(scheme) {
466
+ const dark = scheme !== 'light'
467
+ const colors = tokenColors()
468
+ const rules = tokenRules()
469
+ const layered = withOverrides(dark ? DARK_RULES : LIGHT_RULES, rules)
470
+ return {
471
+ base: dark ? 'vs-dark' : 'vs',
472
+ rules: withBaseForeground(layered, colors['editor.foreground']),
473
+ colors: Object.assign({}, dark ? DARK_COLORS : LIGHT_COLORS, colors),
474
+ hasTokens: Object.keys(colors).length > 0 || Object.keys(rules).length > 0,
475
+ }
476
+ }
477
+
478
+ /**
479
+ * 应用跟随官方的主题;令牌读不到或定义失败时回落现役双套。
480
+ * @author ddj 2026年09月10号
481
+ * @param monaco Monaco 实例
482
+ * @returns 实际应用的 Monaco 主题 id
483
+ */
484
+ export function applyOfficial(monaco) {
485
+ if (!monaco?.editor?.setTheme || !monaco?.editor?.defineTheme) return themeNameOf()
486
+ const theme = officialThemeOf(detectColorScheme())
487
+ if (!theme.hasTokens) {
488
+ applyTheme(monaco)
489
+ return themeNameOf()
490
+ }
491
+ officialSeq += 1
492
+ const id = EDRV_OFFICIAL + '-' + officialSeq
493
+ try {
494
+ monaco.editor.defineTheme(id, { base: theme.base, inherit: true, rules: theme.rules, colors: theme.colors })
495
+ monaco.editor.setTheme(id)
496
+ return id
497
+ } catch (error) {
498
+ applyTheme(monaco)
499
+ return themeNameOf()
500
+ }
501
+ }
502
+
503
+ /**
504
+ * 观察官方明暗标记变化(body / 根节点的 data-ds-dark-theme 与 data-theme)。
505
+ * @author ddj 2026年09月10号
506
+ * @param onChange 标记变化回调
507
+ * @returns 停止观察函数(非浏览器环境返回空函数)
508
+ */
509
+ export function observeScheme(onChange) {
510
+ try {
511
+ if (typeof MutationObserver !== 'function' || !document?.body) return () => {}
512
+ const observer = new MutationObserver(() => onChange())
513
+ const filter = ['data-ds-dark-theme', 'data-theme']
514
+ observer.observe(document.body, { attributes: true, attributeFilter: filter })
515
+ if (document.documentElement) {
516
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: filter })
517
+ }
518
+ return () => observer.disconnect()
519
+ } catch (error) {
520
+ return () => {}
521
+ }
522
+ }
523
+
524
+ /**
525
+ * 官方主题快照 → 明暗(ThemeRuntime.getTheme() 快照;字段缺失/未知返回 undefined)。
526
+ * @author ddj 2026年09月10号
527
+ * @param snapshot 官方主题快照
528
+ * @returns 'light' | 'dark' | undefined
529
+ */
530
+ export function schemeOfSnapshot(snapshot) {
531
+ const value = snapshot ?? {}
532
+ const scheme = value.active?.colorScheme ?? value.colorScheme
533
+ return scheme === 'light' || scheme === 'dark' ? scheme : undefined
534
+ }
535
+ // --endregion
@@ -37,6 +37,8 @@ export const OFFICIAL_FILE_KIND = 'edrvEditorFile'
37
37
  export interface OfficialTabParams {
38
38
  openPath?: string
39
39
  focusDiff?: boolean
40
+ /** 目标行号(行引用/工具行跳转透传;缺省仅打开文件)。 */
41
+ line?: number
40
42
  }
41
43
 
42
44
  /** sidebarRightTabs 服务的最小结构面(结构性探测,不 import 官方类型)。 */
@@ -97,6 +99,19 @@ export function resolveNavOpen(params: unknown): { path: string | null; focusDif
97
99
  return { path, focusDiff: value?.focusDiff === true }
98
100
  }
99
101
 
102
+ /**
103
+ * 从官方导航参数安全解析行号(缺字段/坏类型/非正数一律视为未指定)。
104
+ * @author ddj 2026年09月10号
105
+ * @param params openTab/openResource 传入的 params(正文经 navigation.params 读回)
106
+ * @returns 正整数行号,或 undefined
107
+ */
108
+ export function resolveNavLine(params: unknown): number | undefined {
109
+ const value = (params ?? {}) as OfficialTabParams
110
+ const line = value?.line
111
+ if (typeof line !== 'number' || !Number.isFinite(line) || line <= 0) return undefined
112
+ return Math.floor(line)
113
+ }
114
+
100
115
  /** 逐段 component 解码(失败返回 null,对齐官方 parseFileAddress 的容错语义)。 */
101
116
  function decodeSegment(raw: string | undefined): string | null {
102
117
  if (raw === undefined) return null
@@ -179,6 +194,36 @@ export function buildFileAddress(path: string, sessionId?: string): string {
179
194
  return OFFICIAL_FILE_PREFIX + 'session/' + encodeSegment(sessionId) + '/' + relative.split('/').map(encodeSegment).join('/')
180
195
  }
181
196
 
197
+ // --region file 认领转发(文件分页收归编辑器自带页签栏)
198
+
199
+ /**
200
+ * 把 file 打开请求转发进单一编辑器页签(文件分页由编辑器自带页签栏接管,
201
+ * 官方侧栏不再按文件分裂出多套编辑器实例)。
202
+ * @author ddj 2026年09月10号
203
+ * @param service 官方 sidebarRight 服务(缺失/缺 openTab 返回 false)
204
+ * @param path 目标文件路径(空值返回 false)
205
+ * @param line 可选行号(透传给编辑器页签导航参数)
206
+ * @returns 转发是否成功(openTab 抛错返回 false,由调用方决定重试或兜底)
207
+ */
208
+ export function forwardToEditor(
209
+ service: SidebarRightServiceLike | undefined | null,
210
+ path: string | null,
211
+ line?: number,
212
+ ): boolean {
213
+ if (!service || typeof service.openTab !== 'function' || !path) return false
214
+ const params: OfficialTabParams = { openPath: path }
215
+ if (line !== undefined) params.line = line
216
+ try {
217
+ // 官方语义:页类型恒去重,故转发只会聚焦既有编辑器页签,不产生新实例
218
+ service.openTab(OFFICIAL_TAB_KIND, { params })
219
+ return true
220
+ } catch (error) {
221
+ log.warn('file 认领转发进编辑器页签失败(' + String(error) + ')')
222
+ return false
223
+ }
224
+ }
225
+ // --endregion
226
+
182
227
  /**
183
228
  * 注册官方 Tab 正文(keyed slot,key=定义 id;slots.inject 等待 rightbar seat 声明)。
184
229
  * @author ddj 2026年09月09号
@@ -5,19 +5,20 @@
5
5
  * 作者 ddj 2026-08-27
6
6
  */
7
7
  import React from 'react'
8
+ import { IconCodeOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
8
9
  import { OutlinePanel } from './OutlinePanel.js'
9
10
  import type { SidebarPanelDef, SidebarCtx } from '../sidebar/types.js'
10
11
 
11
12
  /**
12
13
  * 构造大纲面板定义。
13
- * @author ddj 2026年08月27号
14
- * @returns 面板定义(无徽标;活动栏图标 📜)
14
+ * @author ddj 2026年08月27号 / 2026年09月10号
15
+ * @returns 面板定义(无徽标;活动栏图标 = 官方 IconCodeOutline16)
15
16
  */
16
17
  export function createOutlinePanel(): SidebarPanelDef {
17
18
  return {
18
19
  id: 'outline',
19
20
  title: '大纲',
20
- icon: '📜',
21
+ icon: IconCodeOutline16,
21
22
  order: 20,
22
23
  render: (ctx: SidebarCtx) => React.createElement(OutlinePanel, { ctx }),
23
24
  }
@@ -18,6 +18,21 @@ const SEAM_W = 5
18
18
  /** 隐藏态拖出面板区的触发阈值(px,拖过即展开并继续实时调宽)。 */
19
19
  const PULL_OUT_THRESHOLD = 12
20
20
 
21
+ /**
22
+ * 活动栏图标内容:面板可给官方图标组件(跟随官方主题),也可给文本/emoji(回落)。
23
+ * @author ddj 2026年09月10号
24
+ * @param icon 面板定义的 icon 字段(组件或文本)
25
+ * @returns 图标元素或文本;组件渲染异常返回 null
26
+ */
27
+ function railIconEl(icon) {
28
+ if (typeof icon !== 'function') return icon
29
+ try {
30
+ return React.createElement(icon, { size: 16 })
31
+ } catch (error) {
32
+ return null
33
+ }
34
+ }
35
+
21
36
  /**
22
37
  * 侧边栏容器。
23
38
  * @param props.registry 面板注册表(list() 提供面板顺序)
@@ -112,7 +127,7 @@ export function SidebarView(props) {
112
127
  if (!visible && typeof onShow === 'function') onShow()
113
128
  },
114
129
  },
115
- React.createElement('span', { className: 'edrv-rail-icon' }, p.icon),
130
+ React.createElement('span', { className: 'edrv-rail-icon' }, railIconEl(p.icon)),
116
131
  count > 0
117
132
  ? React.createElement('span', { className: 'edrv-rail-badge' }, String(count))
118
133
  : null)