dsh-vscode-mode 0.1.62 → 0.1.63
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/lib/client.js +639 -48
- package/lib/client.js.map +1 -1
- package/lib/index.js +3 -1
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +23 -4
- package/src/client/keybindings.ts +2 -0
- package/src/client/monaco/loader.ts +3 -3
- package/src/client/monaco/theme.ts +313 -7
- package/src/client/officialSidebar.ts +45 -0
- package/src/client/outline/index.ts +4 -3
- package/src/client/sidebar/SidebarView.ts +16 -1
- package/src/client/sidebar/panels/FileExplorer.ts +58 -4
- package/src/client/sidebar/panels/index.ts +12 -10
- package/src/client/sidebar/types.ts +2 -2
- package/src/client/styles/editor.css +33 -28
- package/src/client/ui/ClaimRouter.ts +96 -0
- package/src/client/ui/EditorView.ts +63 -12
- package/src/client/ui/OfficialSideTab.ts +8 -9
- package/src/client-primitives.d.ts +37 -0
- package/src/shared/keybindings.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.63",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
package/src/client/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { ConversationDiffDock } from './ui/ConversationDiffDock.js'
|
|
|
22
22
|
import { McpSettings } from './ui/McpSettings.js'
|
|
23
23
|
import { rpc } from './rpc.js'
|
|
24
24
|
import { loadMonaco } from './monaco/loader.js'
|
|
25
|
+
import { observeScheme, schemeOfSnapshot } from './monaco/theme.js'
|
|
25
26
|
import { createFileOpenerRegistry, scanSidebar, officialSidebarOpener, shouldClaimFiles, type FileOpenContext } from './fileOpeners.js'
|
|
26
27
|
import type { FileOpenerRegistry } from './fileOpeners.js'
|
|
27
28
|
import { installOpenPathRouter, vscodeOpener, autoValue } from './openPathRouter.js'
|
|
@@ -33,6 +34,7 @@ import { detectSidebarService, installSideEditor, setEnsureSideEditor, SIDEBAR_I
|
|
|
33
34
|
import { detectOfficial, installOfficial, registerOfficialFileClaim, OFFICIAL_TAB_KIND, OFFICIAL_TAB_TITLE, isEditorTabActive, restoreEditorTab } from './officialSidebar.js'
|
|
34
35
|
import { SideEditorTab } from './ui/SideEditorTab.js'
|
|
35
36
|
import { OfficialSideTab } from './ui/OfficialSideTab.js'
|
|
37
|
+
import { createClaimRouter } from './ui/ClaimRouter.js'
|
|
36
38
|
import { createAddToConversation } from './addToConversation.js'
|
|
37
39
|
import { createSidebarPanelRegistry } from './sidebar/registry.js'
|
|
38
40
|
import { createFilePanel } from './sidebar/panels/index.js'
|
|
@@ -148,7 +150,7 @@ export function apply(ctx: any): void {
|
|
|
148
150
|
{ name: '侧边栏打开器(' + SIDEBAR_PLUGIN + ')', active: registry.get(SIDEBAR_PLUGIN) !== undefined, note: registry.get(SIDEBAR_PLUGIN) !== undefined ? '已注册(优先级 80)' : '未检测到侧边栏打开能力' },
|
|
149
151
|
{ name: '侧边栏编辑区(官方 Sidebar)', active: officialService !== undefined, note: officialService !== undefined ? '编辑区=官方右侧 Sidebar Tab(对话+编辑同屏,DSH 0.1.5+)' : '官方侧边栏服务未探测到(DSH < 0.1.5-alpha.1 时属预期)' },
|
|
150
152
|
{ name: '侧边栏编辑区(' + SIDEBAR_PLUGIN + ',归档)', active: sideService !== undefined, note: sideService !== undefined ? '编辑区=侧边栏 Tab(旧版 DSH 回退形态)' : '未检测到;DSH ≥ 0.1.5 优先官方侧边栏,旧版可安装(' + SIDEBAR_INSTALL_CMD + ')' },
|
|
151
|
-
{ name: '文件链接官方认领(dsh-resource://file)', active: claimDisposer !== null, note: claimDisposer !== null ? '
|
|
153
|
+
{ name: '文件链接官方认领(dsh-resource://file)', active: claimDisposer !== null, note: claimDisposer !== null ? '聊天文件链接转发进单一编辑器页签(文件分页归编辑器自带页签栏)' : '链接走官方查看器或旧版路由(未认领)' },
|
|
152
154
|
]
|
|
153
155
|
|
|
154
156
|
ctx.provide('fileOpeners', registry)
|
|
@@ -183,14 +185,16 @@ export function apply(ctx: any): void {
|
|
|
183
185
|
const sidebar = scanSidebar(ctx)
|
|
184
186
|
return sidebar ? registry.register(sidebar) : undefined
|
|
185
187
|
}, 'vscode-mode: sidebar file opener')
|
|
186
|
-
//
|
|
188
|
+
// 官方侧边栏正文组件装配(页类型正文;兼作 file 认领转发失败时的兜底正文)
|
|
187
189
|
const officialRenderTab = (props: Record<string, unknown>) => React.createElement(OfficialSideTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions }))
|
|
188
|
-
/** 官方 file 地址认领同步:自动/VSCodeMode
|
|
190
|
+
/** 官方 file 地址认领同步:自动/VSCodeMode 档认领(转发进单一编辑器页签),其余交官方查看器。 */
|
|
189
191
|
const syncFileClaim = (): void => {
|
|
190
192
|
const official = officialService
|
|
191
193
|
const want = official !== undefined && shouldClaimFiles(selected)
|
|
192
194
|
if (want && claimDisposer === null && official) {
|
|
193
|
-
|
|
195
|
+
// 认领正文只做转发:文件分页收归编辑器自带页签栏,官方侧栏不再按文件分裂编辑器实例
|
|
196
|
+
const renderTab = createClaimRouter({ service: official.service, schedule, fallback: officialRenderTab })
|
|
197
|
+
const disposer = registerOfficialFileClaim({ tabs: official.tabs, slots: ctx.slots, renderTab })
|
|
194
198
|
if (disposer !== null) claimDisposer = ctx.effect(() => disposer, 'vscode-mode: official file claim')
|
|
195
199
|
return
|
|
196
200
|
}
|
|
@@ -265,6 +269,21 @@ export function apply(ctx: any): void {
|
|
|
265
269
|
}
|
|
266
270
|
retryRemoteOpen()
|
|
267
271
|
|
|
272
|
+
// 官方主题跟随(DSH 0.1.5+):ctx.theme 快照事件优先,其次明暗标记观察(observeScheme),
|
|
273
|
+
// 都不可用时 theme.ts 的 detectColorScheme 兜底;统一广播 edrv:theme-change 触发 Monaco 主题重刷。
|
|
274
|
+
const themeService = ctx.get('theme') as { getTheme?: () => unknown } | undefined
|
|
275
|
+
const emitTheme = (): void => {
|
|
276
|
+
const snapshot = typeof themeService?.getTheme === 'function' ? themeService.getTheme() : undefined
|
|
277
|
+
window.dispatchEvent(new CustomEvent('edrv:theme-change', { detail: { scheme: schemeOfSnapshot(snapshot) } }))
|
|
278
|
+
}
|
|
279
|
+
ctx.effect(() => {
|
|
280
|
+
if (!themeService || typeof ctx.on !== 'function') return undefined
|
|
281
|
+
const disposer = ctx.on('theme/change', emitTheme)
|
|
282
|
+
return typeof disposer === 'function' ? disposer : undefined
|
|
283
|
+
}, 'vscode-mode: official theme event')
|
|
284
|
+
ctx.effect(() => observeScheme(emitTheme), 'vscode-mode: theme attribute watch')
|
|
285
|
+
emitTheme()
|
|
286
|
+
|
|
268
287
|
// 中央「文件编辑」页签(旧形态回退):类 VSCode 编辑器;侧边栏形态(官方/better-sidebar)可用时
|
|
269
288
|
// 编辑器住侧边栏 Tab,本页签不注册(避免双实例:Monaco×2 + diff dock 每会话单源抢占)。
|
|
270
289
|
const registerLegacyTab = (): (() => void) | null => {
|
|
@@ -25,6 +25,8 @@ export const COMMANDS: Array<{ id: string; label: string }> = [
|
|
|
25
25
|
{ id: 'edrv.searchInFiles', label: '在工作区中搜索' },
|
|
26
26
|
{ id: 'edrv.navigateBack', label: '后退(导航历史)' },
|
|
27
27
|
{ id: 'edrv.navigateForward', label: '前进(导航历史)' },
|
|
28
|
+
{ id: 'edrv.nextTab', label: '下一个页签' },
|
|
29
|
+
{ id: 'edrv.prevTab', label: '上一个页签' },
|
|
28
30
|
]
|
|
29
31
|
|
|
30
32
|
const MODIFIERS: Record<string, 'ctrl' | 'shift' | 'alt' | 'meta'> = {
|
|
@@ -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 {
|
|
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
|
-
//
|
|
105
|
+
// 主题:注册现役双套(幂等)+ 应用跟随官方的令牌主题(令牌缺失时回落现役)
|
|
106
106
|
try {
|
|
107
107
|
registerThemes(window.monaco)
|
|
108
|
-
|
|
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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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)
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* host 树索引,命中近乎零成本);挂载/切会话从缓存即时恢复展开树;⟳/edrv:refresh
|
|
6
6
|
* 保留旧条目强制后台重列(force);每次用户发起加载后预取 ≤4 个子目录(排除重型
|
|
7
7
|
* 目录、缓存新鲜跳过、不级联);10s 轻量跟随已展开目录(命中索引,RPC 近零成本)。
|
|
8
|
-
*
|
|
9
|
-
* 作者 ddj 2026-08-26 / 2026-08-27 / 2026-08-31
|
|
8
|
+
* 差异角标/右键菜单/展开状态持久化行为不变;行图标用官方原语(文件夹图标 + 文件类型图标)。
|
|
9
|
+
* 作者 ddj 2026-08-26 / 2026-08-27 / 2026-08-31 / 2026-09-10
|
|
10
10
|
*/
|
|
11
11
|
import React from 'react'
|
|
12
|
+
import { FileTypeIcon, IconFolderClose16, IconFolderOpen16, IconRefreshOutline16, classifyFileType } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
12
13
|
import { rpc } from '../../rpc.js'
|
|
13
14
|
import { ContextMenu } from '../../ui/ContextMenu.js'
|
|
14
15
|
import { buildTreeMenu } from '../contextMenu.js'
|
|
@@ -23,6 +24,58 @@ const FOLLOW_INTERVAL_MS = 10_000
|
|
|
23
24
|
const PREFETCH_MAX = 4
|
|
24
25
|
const PREFETCH_EXCLUDED = new Set(['node_modules', '.git', '.hg', '.svn', '.pnpm', '.pnpm-store'])
|
|
25
26
|
|
|
27
|
+
// --region 行图标(官方原语:目录文件夹图标 + 文件类型图标;缺失时回落纯文本)
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 目录图标元素(官方 IconFolderOpen16/Close16)。
|
|
31
|
+
* @author ddj 2026年09月10号
|
|
32
|
+
* @param isOpen 目录是否展开
|
|
33
|
+
* @returns 图标元素;原语缺失返回 null
|
|
34
|
+
*/
|
|
35
|
+
function dirIconEl(isOpen) {
|
|
36
|
+
const Icon = isOpen ? IconFolderOpen16 : IconFolderClose16
|
|
37
|
+
if (typeof Icon !== 'function') return null
|
|
38
|
+
return React.createElement(Icon, { size: 16, className: 'edrv-tree-icon' })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 文件图标元素(官方 FileTypeIcon + classifyFileType 分类,未知类型为 other)。
|
|
43
|
+
* @author ddj 2026年09月10号
|
|
44
|
+
* @param name 文件名(含扩展名)
|
|
45
|
+
* @returns 图标元素;原语缺失或分类异常返回 null
|
|
46
|
+
*/
|
|
47
|
+
function fileIconEl(name) {
|
|
48
|
+
if (typeof FileTypeIcon !== 'function' || typeof classifyFileType !== 'function') return null
|
|
49
|
+
try {
|
|
50
|
+
return React.createElement(FileTypeIcon, { kind: classifyFileType(String(name ?? '')), size: 16, className: 'edrv-tree-icon' })
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 行图标分派:目录 → 文件夹图标,文件 → 官方类型图标。
|
|
58
|
+
* @author ddj 2026年09月10号
|
|
59
|
+
* @param e 目录条目(name/type)
|
|
60
|
+
* @param isDir 是否目录
|
|
61
|
+
* @param isOpen 目录是否展开
|
|
62
|
+
* @returns 图标元素或 null
|
|
63
|
+
*/
|
|
64
|
+
function rowIconEl(e, isDir, isOpen) {
|
|
65
|
+
return isDir ? dirIconEl(isOpen) : fileIconEl(e?.name)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 刷新按钮图标(官方 IconRefreshOutline16;原语缺失回落文本 ⟳)。
|
|
70
|
+
* @author ddj 2026年09月10号
|
|
71
|
+
* @returns 图标元素或回落文本
|
|
72
|
+
*/
|
|
73
|
+
function refreshIconEl() {
|
|
74
|
+
if (typeof IconRefreshOutline16 !== 'function') return '⟳'
|
|
75
|
+
return React.createElement(IconRefreshOutline16, { size: 14 })
|
|
76
|
+
}
|
|
77
|
+
// --endregion
|
|
78
|
+
|
|
26
79
|
/**
|
|
27
80
|
* 目录树面板主体(SWR:有缓存先渲染,无缓存才显示加载态;加载总在后台)。
|
|
28
81
|
* @param props.ctx 面板共享上下文(sessionId/openFile/activePath/pendingByPath/fileMenuItems/notify)
|
|
@@ -227,8 +280,9 @@ export function FileExplorer(props) {
|
|
|
227
280
|
},
|
|
228
281
|
React.createElement('span', { className: 'edrv-tree-chev' },
|
|
229
282
|
isDir ? (isOpen ? '▾' : '▸') : ''),
|
|
283
|
+
rowIconEl(e, isDir, isOpen),
|
|
230
284
|
React.createElement('span', { className: 'edrv-tree-name' + (dim ? ' edrv-tree-dim' : '') },
|
|
231
|
-
|
|
285
|
+
e.name),
|
|
232
286
|
(pending > 0
|
|
233
287
|
? React.createElement('span', { className: 'edrv-tree-badge' }, String(pending))
|
|
234
288
|
: null))
|
|
@@ -283,7 +337,7 @@ export function FileExplorer(props) {
|
|
|
283
337
|
React.createElement('span', { className: 'edrv-side-title' }, '资源管理器'),
|
|
284
338
|
React.createElement('span', { className: 'edrv-side-root', title: root || '' }, rootName),
|
|
285
339
|
React.createElement('span', { style: { flex: 1 } }),
|
|
286
|
-
React.createElement('button', { className: 'edrv-side-btn', title: '刷新目录树', onClick: refresh },
|
|
340
|
+
React.createElement('button', { className: 'edrv-side-btn', title: '刷新目录树', onClick: refresh }, refreshIconEl())),
|
|
287
341
|
React.createElement('div', { className: 'edrv-tree' },
|
|
288
342
|
(errorText
|
|
289
343
|
? React.createElement('div', { className: 'edrv-tree-error' },
|