dsh-vscode-mode 0.1.20 → 0.1.22

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.
@@ -0,0 +1,134 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — 大纲源注册表 + 内置数据源(monaco / fallback)。
4
+ * 解析规则:按优先级降序遍历源,首个非空结果生效;全空 → 空态。
5
+ * monaco 源吃 Monaco 原生 document symbol provider(ts/json/css/html 与未来
6
+ * 第三方注册的 provider);fallback 源兜底无内置提供方的语言(parse.ts)。
7
+ * 注册表对外 provide 为 edrvOutlineSources,第三方语言插件可注册更高优先级源。
8
+ * 作者 ddj 2026-08-27
9
+ */
10
+ import type { OutlineSourceRegistry, OutlineSource, OutlineSourceInput, OutlineSymbol } from './types.js'
11
+ import { parseOutline } from './parse.js'
12
+
13
+ /** Monaco 原生有 document symbol provider 的语言(无需 fallback)。 */
14
+ export const OUTLINE_MONACO_LANGS = new Set([
15
+ 'typescript', 'javascript', 'json', 'jsonc', 'css', 'scss', 'less', 'html',
16
+ ])
17
+
18
+ /** 无内置 provider、由 parse.ts 兜底的语言。 */
19
+ export const OUTLINE_FALLBACK_LANGS = new Set([
20
+ 'markdown', 'mdx', 'python', 'shell', 'powershell', 'lua', 'go', 'rust',
21
+ 'yaml', 'ini', 'c', 'cpp', 'csharp', 'java', 'php', 'ruby', 'kotlin',
22
+ 'swift', 'dart',
23
+ ])
24
+
25
+ /**
26
+ * 创建生命周期独立的大纲源注册表。
27
+ * @author ddj 2026年08月27号
28
+ * @returns 大纲源注册表
29
+ */
30
+ export function createOutlineSourceRegistry(): OutlineSourceRegistry {
31
+ const entries = new Map<string, OutlineSource>()
32
+ const listeners = new Set<() => void>()
33
+ const notify = (): void => {
34
+ for (const listener of listeners) listener()
35
+ }
36
+ const list = (): readonly OutlineSource[] =>
37
+ [...entries.values()].sort((a, b) => (b.priority - a.priority) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
38
+ return {
39
+ register(source: OutlineSource): () => void {
40
+ if (!source || !source.id || typeof source.get !== 'function') throw new TypeError('大纲源必须提供 id 与 get')
41
+ entries.set(source.id, source)
42
+ notify()
43
+ return () => {
44
+ if (entries.get(source.id) !== source) return
45
+ entries.delete(source.id)
46
+ notify()
47
+ }
48
+ },
49
+ list,
50
+ subscribe(listener: () => void): () => void {
51
+ listeners.add(listener)
52
+ return () => listeners.delete(listener)
53
+ },
54
+ get: (id: string) => entries.get(id),
55
+ }
56
+ }
57
+
58
+ /** 归一化 Monaco OutlineElement(getTopLevelSymbols 产物)→ OutlineSymbol。 */
59
+ function normalizeMonaco(el): OutlineSymbol | null {
60
+ if (!el || typeof el.name !== 'string') return null
61
+ const range = el.range || {}
62
+ const sel = el.selectionRange || range
63
+ const startLine = Math.max(1, Number(sel.startLineNumber ?? range.startLineNumber ?? 1))
64
+ const endLine = Math.max(1, Number(range.endLineNumber ?? startLine))
65
+ const out: OutlineSymbol = {
66
+ name: el.name,
67
+ kind: typeof el.kind === 'number' ? el.kind : 0,
68
+ startLine: Math.max(1, Number(range.startLineNumber ?? startLine)),
69
+ endLine,
70
+ selectLine: startLine,
71
+ }
72
+ if (typeof el.detail === 'string' && el.detail) out.detail = el.detail
73
+ if (Array.isArray(el.children) && el.children.length) {
74
+ const kids = el.children.map(normalizeMonaco).filter(Boolean)
75
+ if (kids.length) out.children = kids
76
+ }
77
+ return out
78
+ }
79
+
80
+ /**
81
+ * 按优先级解析大纲符号:首个非空结果生效(出错源跳过,落入下一优先级)。
82
+ * @author ddj 2026年08月27号
83
+ * @param sources 大纲源注册表
84
+ * @param input 当前快照(languageId/model/editor/monaco)
85
+ * @returns 归一化符号树(无符号时为空数组)
86
+ */
87
+ export async function resolveOutline(sources: OutlineSourceRegistry, input: OutlineSourceInput): Promise<OutlineSymbol[]> {
88
+ for (const source of sources.list()) {
89
+ let supports = false
90
+ try { supports = source.provides(input.languageId) } catch { supports = false }
91
+ if (!supports) continue
92
+ let items: OutlineSymbol[] | null = null
93
+ try { items = await source.get(input) } catch { items = null }
94
+ if (items && items.length) return items
95
+ }
96
+ return []
97
+ }
98
+
99
+ /**
100
+ * 注册内置大纲源(monaco=50 / fallback=30)。
101
+ * @author ddj 2026年08月27号
102
+ * @param registry 目标注册表
103
+ * @returns 注销函数(同时注销全部内置源)
104
+ */
105
+ export function registerBuiltinOutlineSources(registry: OutlineSourceRegistry): () => void {
106
+ const disposers = [
107
+ registry.register({
108
+ id: 'monaco',
109
+ priority: 50,
110
+ provides: () => true,
111
+ async get(input: OutlineSourceInput): Promise<OutlineSymbol[]> {
112
+ const ed = input.editor
113
+ const model = input.model
114
+ const cmd = ed?._commandService
115
+ if (!cmd || typeof cmd.executeCommand !== 'function') return []
116
+ const uri = model?.uri
117
+ if (!uri) return []
118
+ const list = await cmd.executeCommand('_executeDocumentSymbolProvider', uri)
119
+ return Array.isArray(list) ? list.map(normalizeMonaco).filter(Boolean) : []
120
+ },
121
+ }),
122
+ registry.register({
123
+ id: 'fallback',
124
+ priority: 30,
125
+ provides: (languageId: string) => OUTLINE_FALLBACK_LANGS.has(languageId),
126
+ async get(input: OutlineSourceInput): Promise<OutlineSymbol[]> {
127
+ const model = input.model
128
+ if (!model || typeof model.getValue !== 'function') return []
129
+ return parseOutline(input.languageId, model.getValue())
130
+ },
131
+ }),
132
+ ]
133
+ return () => { for (const dispose of disposers) dispose() }
134
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * dsh-vscode-mode client — 大纲契约(归一化符号模型 + 数据源接口)。
3
+ * 面板只消费 OutlineSymbol[],与 Monaco 解耦;未来语言插件(LSP/VSIX 风格,
4
+ * 如 EmmyLua/C#)注册高优先级 OutlineSource 即可接入,无需改动面板。
5
+ * kind 取 monaco SymbolKind 数值(0–25,与 LSP SymbolKind 一致)。
6
+ * 作者 ddj 2026-08-27
7
+ */
8
+
9
+ /** 归一化大纲符号(面板唯一渲染契约)。 */
10
+ export interface OutlineSymbol {
11
+ name: string
12
+ /** monaco SymbolKind 数值:File0/Module1/Namespace2/Package3/Class4/Method5/Property6/Field7/Constructor8/Enum9/Interface10/Function11/Variable12/Constant13/String14/Number15/Boolean16/Array17/Object18/Key19/Null20/EnumMember21/Struct22/Event23/Operator24/TypeParameter25。 */
13
+ kind: number
14
+ detail?: string
15
+ /** 符号整体起始行(1-based)。 */
16
+ startLine: number
17
+ /** 符号整体结束行(1-based)。 */
18
+ endLine: number
19
+ /** 跳转行(符号名所在行,1-based)。 */
20
+ selectLine: number
21
+ children?: OutlineSymbol[]
22
+ }
23
+
24
+ /** 大纲数据源输入(面板解析时注入当前快照;字段均为弱类型,源自行 duck-typing)。 */
25
+ export interface OutlineSourceInput {
26
+ languageId: string
27
+ /** Monaco 文本模型(源可用 model.getValue() 自行解析)。 */
28
+ model?: unknown
29
+ /** 当前 Monaco 编辑器实例(供 _commandService 等内部调用)。 */
30
+ editor?: unknown
31
+ /** 全局 monaco 对象。 */
32
+ monaco?: unknown
33
+ }
34
+
35
+ /** 大纲数据源:按优先级降序解析,首个非空结果生效。 */
36
+ export interface OutlineSource {
37
+ id: string
38
+ /** 数值越大越优先(内置:monaco=50、fallback=30;第三方语言插件建议 60–90)。 */
39
+ priority: number
40
+ /** 是否可为该语言提供符号。 */
41
+ provides(languageId: string): boolean
42
+ get(input: OutlineSourceInput): Promise<OutlineSymbol[]>
43
+ }
44
+
45
+ /** 大纲源注册表(ctx.provide 为 edrvOutlineSources,供第三方注册)。 */
46
+ export interface OutlineSourceRegistry {
47
+ register(source: OutlineSource): () => void
48
+ list(): readonly OutlineSource[]
49
+ subscribe(listener: () => void): () => void
50
+ get(id: string): OutlineSource | undefined
51
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * dsh-vscode-mode client — 文件管理右键菜单框架(类型 + 注册表 + 纯构建函数)。
3
+ * 镜像 sidebar/registry.ts 的注册表模式:create + register(返回注销) + list + subscribe,
4
+ * 由 client/index.ts `ctx.provide('edrvFileContextMenuItems', registry)` 对外暴露(第三方可注册)。
5
+ * 新增菜单项只需 register 一条 TreeMenuItem,面板在右键打开时经 buildTreeMenu 过滤/排序。
6
+ * 本模块不触 React/浏览器,可单测。
7
+ * 作者 ddj 2026-08-27
8
+ */
9
+ import type { TreeEntry } from '../../shared/rpc.js'
10
+ import type { SidebarCtx } from './types.js'
11
+
12
+ /** 右键目标(树行/面板空白区,path 相对工作区根,'' 表示根目录)。 */
13
+ export interface TreeMenuTarget {
14
+ path: string
15
+ type: TreeEntry['type']
16
+ }
17
+
18
+ /** 一条右键菜单项定义。 */
19
+ export interface TreeMenuItem {
20
+ id: string
21
+ label: string
22
+ /** 排序:数值越小越靠前(缺省 100)。 */
23
+ order?: number
24
+ /** 红色警示样式(如删除类操作)。 */
25
+ danger?: boolean
26
+ /** 置灰不可点(如无权限/目标不支持时)。 */
27
+ disabled?: boolean
28
+ /** 前置分隔线。 */
29
+ separator?: boolean
30
+ /** 显隐守卫:返回 false 则不显示。 */
31
+ visible?: (target: TreeMenuTarget, ctx: SidebarCtx) => boolean
32
+ run: (target: TreeMenuTarget, ctx: SidebarCtx) => void
33
+ }
34
+
35
+ /** 右键菜单项注册表(生命周期独立,可注册/注销/订阅)。 */
36
+ export interface TreeMenuRegistry {
37
+ register(item: TreeMenuItem): () => void
38
+ list(): readonly TreeMenuItem[]
39
+ subscribe(listener: () => void): () => void
40
+ get(id: string): TreeMenuItem | undefined
41
+ }
42
+
43
+ /** 校验菜单项并写入注册表(缺 id/run/label 抛 TypeError)。 */
44
+ function itemRegister(entries: Map<string, TreeMenuItem>, notify: () => void, item: TreeMenuItem): void {
45
+ if (!item.id || typeof item.run !== 'function' || !item.label) {
46
+ throw new TypeError('文件右键菜单项必须提供 id、label 和 run')
47
+ }
48
+ entries.set(item.id, item)
49
+ notify()
50
+ }
51
+
52
+ /**
53
+ * 创建生命周期独立的右键菜单项注册表。
54
+ * @author ddj 2026年08月27号
55
+ * @returns 菜单项注册表
56
+ */
57
+ export function createTreeMenuRegistry(): TreeMenuRegistry {
58
+ const entries = new Map<string, TreeMenuItem>()
59
+ const listeners = new Set<() => void>()
60
+ const notify = (): void => {
61
+ for (const listener of listeners) listener()
62
+ }
63
+ const list = (): readonly TreeMenuItem[] =>
64
+ [...entries.values()].sort((a, b) => (a.order ?? 100) - (b.order ?? 100))
65
+ return {
66
+ register(item: TreeMenuItem): () => void {
67
+ itemRegister(entries, notify, item)
68
+ return () => {
69
+ if (entries.get(item.id) !== item) return
70
+ entries.delete(item.id)
71
+ notify()
72
+ }
73
+ },
74
+ list,
75
+ subscribe(listener: () => void): () => void {
76
+ listeners.add(listener)
77
+ return () => listeners.delete(listener)
78
+ },
79
+ get: (id: string) => entries.get(id),
80
+ }
81
+ }
82
+
83
+ /**
84
+ * 按目标构建当前可见菜单项(visible 过滤 + order 排序)。
85
+ * @author ddj 2026年08月27号
86
+ * @param registry 菜单项注册表
87
+ * @param target 右键目标
88
+ * @param ctx 面板共享上下文
89
+ * @returns 可见菜单项(已排序)
90
+ */
91
+ export function buildTreeMenu(registry: TreeMenuRegistry | undefined, target: TreeMenuTarget, ctx: SidebarCtx): TreeMenuItem[] {
92
+ if (!registry) return []
93
+ const out: TreeMenuItem[] = []
94
+ for (const item of registry.list()) {
95
+ if (typeof item.visible === 'function' && !item.visible(target, ctx)) continue
96
+ out.push(item)
97
+ }
98
+ return out
99
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * dsh-vscode-mode client — 文件管理右键菜单内置项。
3
+ * 首个内置项:「在文件浏览器中打开」(文件→OS Explorer 定位选中、目录→打开目录)。
4
+ * 反馈统一走 ctx.notify(由 EditorView 提供,落到编辑区路径栏状态)。
5
+ * 作者 ddj 2026-08-27
6
+ */
7
+ import { revealInExplorer } from '../fileReveal.js'
8
+ import type { TreeMenuItem } from './contextMenu.js'
9
+
10
+ /**
11
+ * 构造内置右键菜单项列表(后续内置项直接追加)。
12
+ * @author ddj 2026年08月27号
13
+ * @returns 内置菜单项数组
14
+ */
15
+ export function createDefaultFileMenuItems(): TreeMenuItem[] {
16
+ return [
17
+ {
18
+ id: 'reveal-in-explorer',
19
+ label: '在文件浏览器中打开',
20
+ order: 0,
21
+ run: (target, ctx) => {
22
+ void revealInExplorer(ctx.sessionId, target.path).then((outcome) => {
23
+ ctx.notify?.(outcome.ok ? '已在文件浏览器中打开' : '打开失败:' + (outcome.error ?? '未知错误'))
24
+ })
25
+ },
26
+ },
27
+ ]
28
+ }
@@ -3,17 +3,21 @@
3
3
  * dsh-vscode-mode client — 侧边栏「文件管理」面板(懒加载目录树)。
4
4
  * 点目录展开/收起(首次经 edrv.listDir 拉取并缓存),点文件经 ctx.openFile 打开;
5
5
  * 差异角标取 pendingByPath,活动文件高亮;edrv:refresh 事件与手动刷新重载树。
6
- * 作者 ddj 2026-08-26
6
+ * 右键行/空白弹出菜单:项来自 ctx.fileMenuItems 注册表(可拓展),
7
+ * 内置「在文件浏览器中打开」经 edrv.revealInExplorer 定位/打开。
8
+ * 作者 ddj 2026-08-26 / 2026-08-27
7
9
  */
8
10
  import React from 'react'
9
11
  import { rpc } from '../../rpc.js'
12
+ import { ContextMenu } from '../../ui/ContextMenu.js'
13
+ import { buildTreeMenu } from '../contextMenu.js'
10
14
  import type { SidebarCtx } from '../types.js'
11
15
 
12
16
  const DIR_CAP = 4000
13
17
 
14
18
  /**
15
19
  * 目录树面板主体。
16
- * @param props.ctx 面板共享上下文(sessionId/openFile/activePath/pendingByPath)
20
+ * @param props.ctx 面板共享上下文(sessionId/openFile/activePath/pendingByPath/fileMenuItems/notify)
17
21
  */
18
22
  export function FileExplorer(props) {
19
23
  const ctx = props?.ctx
@@ -26,6 +30,7 @@ export function FileExplorer(props) {
26
30
  const [expanded, setExpanded] = React.useState({})
27
31
  const [loading, setLoading] = React.useState({})
28
32
  const [error, setError] = React.useState(null)
33
+ const [menu, setMenu] = React.useState(null) // 右键菜单 { x, y, target }
29
34
  const tokensRef = React.useRef({})
30
35
  // expanded 的 ref 镜像:edrv:refresh 监听用首次渲染闭包,但需读到最新展开态
31
36
  const expandedRef = React.useRef({})
@@ -113,6 +118,11 @@ export function FileExplorer(props) {
113
118
  title: e.path,
114
119
  style: { paddingLeft: 6 + depth * 14 },
115
120
  onClick: () => { if (isDir) toggle(e.path); else openFile(e.path) },
121
+ onContextMenu: (ev) => {
122
+ ev.preventDefault()
123
+ ev.stopPropagation()
124
+ setMenu({ x: ev.clientX, y: ev.clientY, target: { path: e.path, type: e.type } })
125
+ },
116
126
  },
117
127
  React.createElement('span', { className: 'edrv-tree-chev' },
118
128
  isDir ? (isOpen ? '▾' : '▸') : ''),
@@ -145,7 +155,24 @@ export function FileExplorer(props) {
145
155
  return rows
146
156
  }
147
157
 
148
- return React.createElement('div', { className: 'edrv-side-panel' },
158
+ // 面板区右键(空白处)→ 工作区根目录菜单;行内右键已在 rowEl 阻止冒泡。
159
+ const onPanelContext = (ev) => {
160
+ ev.preventDefault()
161
+ setMenu({ x: ev.clientX, y: ev.clientY, target: { path: '', type: 'directory' } })
162
+ }
163
+ // 当前右键目标的可视菜单项(构建时过滤/排序,registry 变化下次打开生效)。
164
+ const menuEntries = menu
165
+ ? buildTreeMenu(ctx?.fileMenuItems, menu.target, ctx).map((item) => ({
166
+ id: item.id,
167
+ label: item.label,
168
+ danger: item.danger,
169
+ disabled: item.disabled,
170
+ separator: item.separator,
171
+ onClick: () => item.run(menu.target, ctx),
172
+ }))
173
+ : []
174
+
175
+ return React.createElement('div', { className: 'edrv-side-panel', onContextMenu: onPanelContext },
149
176
  React.createElement('div', { className: 'edrv-side-head' },
150
177
  React.createElement('span', { className: 'edrv-side-title' }, '资源管理器'),
151
178
  React.createElement('span', { className: 'edrv-side-root', title: root || '' }, rootName),
@@ -157,5 +184,8 @@ export function FileExplorer(props) {
157
184
  React.createElement('span', null, String(error)),
158
185
  React.createElement('button', { className: 'edrv-side-btn', onClick: () => refresh() }, '重试'))
159
186
  : null),
160
- rowsOf('', 0)))
187
+ rowsOf('', 0)),
188
+ (menu && menuEntries.length
189
+ ? React.createElement(ContextMenu, { x: menu.x, y: menu.y, entries: menuEntries, onClose: () => setMenu(null) })
190
+ : null))
161
191
  }
@@ -3,6 +3,8 @@
3
3
  * 面板 = 活动栏一项 + 面板区一处渲染;新增面板只需注册一条 SidebarPanelDef。
4
4
  * 作者 ddj 2026-08-26
5
5
  */
6
+ import type { OutlineSourceRegistry } from '../outline/types.js'
7
+ import type { TreeMenuRegistry } from './contextMenu.js'
6
8
 
7
9
  /** 面板可用的共享上下文(由 SidebarView 从 EditorView 注入,面板不直碰其内部)。 */
8
10
  export interface SidebarCtx {
@@ -15,6 +17,14 @@ export interface SidebarCtx {
15
17
  sum: { totalFiles: number; files: Array<{ path: string; pending: number }> }
16
18
  /** 触发差异记录刷新(保存/决策后由 EditorView 统一 emitRefresh)。 */
17
19
  refreshRecords: () => void
20
+ /** 取当前 Monaco 编辑器实例(可能为 null;大纲面板读 model/跳转用)。 */
21
+ editor?: () => unknown | null
22
+ /** 大纲源注册表(公开 provide 为 edrvOutlineSources;大纲面板解析符号用)。 */
23
+ outlineSources?: OutlineSourceRegistry
24
+ /** 文件右键菜单项注册表(公开 provide 为 edrvFileContextMenuItems;文件管理面板构建菜单用)。 */
25
+ fileMenuItems?: TreeMenuRegistry
26
+ /** 面板动作反馈(如右键菜单操作结果 → 编辑区路径栏状态)。 */
27
+ notify?: (message: string) => void
18
28
  }
19
29
 
20
30
  /** 单个侧边栏面板定义。 */
@@ -69,6 +69,8 @@
69
69
  [data-edrv-view] .edrv-ctxmenu-item { display: flex; align-items: center; gap: 8px; font-size: 12px; line-height: 18px; padding: 7px 10px; border: none; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-primary, #1f2933); cursor: pointer; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
70
70
  [data-edrv-view] .edrv-ctxmenu-item:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.08)); }
71
71
  [data-edrv-view] .edrv-ctxmenu-item.edrv-ctxmenu-danger { color: var(--dsw-alias-state-error-primary, #d9534f); }
72
+ [data-edrv-view] .edrv-ctxmenu-item:disabled,
73
+ [data-edrv-view] .edrv-ctxmenu-item.edrv-ctxmenu-disabled { color: var(--dsw-alias-label-disabled, #b0b7c0); cursor: default; background: transparent; }
72
74
  [data-edrv-view] .edrv-ctxmenu-sep { height: 1px; margin: 3px 4px; background: var(--dsw-alias-border-l1, #e0e6e8); }
73
75
  [data-edrv-view] .edrv-diffbar-body { max-height: 130px; max-width: min(560px, calc(100vw - 40px)); overflow: auto; padding: 4px 8px 8px; display: flex; flex-direction: column; gap: 4px; border-top: 1px solid var(--dsw-alias-border-l1, #e0e6e8); }
74
76
  [data-edrv-view] .edrv-diffrow { display: flex; align-items: center; gap: 8px; padding: 3px 6px; border: 1px solid var(--dsw-alias-border-l1, #e0e6e8); border-radius: 6px; background: var(--dsw-alias-bg-layer-2, #f0f4f4); cursor: pointer; flex-wrap: wrap; }
@@ -201,7 +203,7 @@
201
203
  [data-edrv-view] .edrv-rail-icon { font-size: 15px; line-height: 1; }
202
204
  [data-edrv-view] .edrv-rail-badge { position: absolute; top: 2px; right: 2px; min-width: 14px; height: 14px; padding: 0 3px; border-radius: 999px; background: var(--dsw-alias-brand-primary, #0f9d58); color: #fff; font-size: 9px; font-weight: 600; line-height: 14px; text-align: center; box-sizing: border-box; }
203
205
  [data-edrv-view] .edrv-side-body { flex: 1; min-width: 0; min-height: 0; display: flex; overflow: hidden; }
204
- [data-edrv-view] .edrv-side-panel { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
206
+ [data-edrv-view] .edrv-side-panel { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; position: relative; }
205
207
  [data-edrv-view] .edrv-side-head { display: flex; align-items: center; gap: 8px; height: 34px; padding: 0 10px 0 12px; border-bottom: 1px solid var(--dsw-alias-border-l1, #e0e6e8); flex-shrink: 0; }
206
208
  [data-edrv-view] .edrv-side-title { font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-primary, #1f2933); white-space: nowrap; }
207
209
  [data-edrv-view] .edrv-side-root { font-size: 11px; color: var(--dsw-alias-label-tertiary, #9aa5b1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@@ -217,6 +219,15 @@
217
219
  [data-edrv-view] .edrv-tree-badge { min-width: 14px; height: 14px; padding: 0 4px; border-radius: 999px; background: var(--dsw-alias-state-warn-primary, #d97706); color: #fff; font-size: 9px; font-weight: 600; line-height: 14px; text-align: center; box-sizing: border-box; flex-shrink: 0; }
218
220
  [data-edrv-view] .edrv-tree-loading { height: 22px; line-height: 22px; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 11px; }
219
221
  [data-edrv-view] .edrv-tree-error { display: flex; align-items: center; gap: 8px; margin: 4px 2px 6px; padding: 6px 8px; border: 1px solid rgba(217,83,79,.4); border-radius: 6px; background: #fdeaea; color: var(--dsw-alias-state-error-primary, #d9534f); font-size: 11px; }
222
+ /* 大纲面板:kind 字形 / detail / 行号 */
223
+ [data-edrv-view] .edrv-outline-kind { display: inline-flex; align-items: center; justify-content: center; width: 14px; flex-shrink: 0; font-size: 12px; font-weight: 600; text-align: center; }
224
+ [data-edrv-view] .edrv-outline-kind.edrv-ol-func { color: var(--dsw-alias-brand-primary, #0f9d58); }
225
+ [data-edrv-view] .edrv-outline-kind.edrv-ol-type { color: var(--dsw-alias-label-primary, #1f2933); }
226
+ [data-edrv-view] .edrv-outline-kind.edrv-ol-data { color: var(--dsw-alias-state-warn-primary, #d97706); }
227
+ [data-edrv-view] .edrv-outline-kind.edrv-ol-ns { color: var(--dsw-alias-label-secondary, #52606d); }
228
+ [data-edrv-view] .edrv-outline-kind.edrv-ol-key { color: var(--dsw-alias-label-tertiary, #9aa5b1); }
229
+ [data-edrv-view] .edrv-outline-detail { flex-shrink: 0; max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 11px; }
230
+ [data-edrv-view] .edrv-outline-ln { flex-shrink: 0; padding-right: 2px; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 10px; font-variant-numeric: tabular-nums; }
220
231
  [data-edrv-view] .edrv-side-resize { width: 5px; flex-shrink: 0; cursor: col-resize; background: transparent; }
221
232
  [data-edrv-view] .edrv-side-resize:hover { background: var(--dsw-alias-brand-primary, #0f9d58); opacity: .35; }
222
233
  [data-edrv-view] .edrv-chip-btn.edrv-chip-on { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.12)); color: var(--dsw-alias-label-primary, #1f2933); }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * dsh-vscode-mode client — 通用浮动右键菜单(createElement 风格、类型化)。
3
+ * 全屏 backdrop(点击/右键/Esc 关闭)+ 固定定位(viewport clamp 防越界),
4
+ * 条目支持 danger/disabled/separator,复用既有 edrv-ctxmenu* 样式类。
5
+ * 作者 ddj 2026-08-27
6
+ */
7
+ import React from 'react'
8
+
9
+ /** 单条菜单项(展示层形状;业务侧由 buildTreeMenu 映射而来)。 */
10
+ export interface ContextMenuEntry {
11
+ id: string
12
+ label: string
13
+ danger?: boolean
14
+ disabled?: boolean
15
+ /** 前置分隔线。 */
16
+ separator?: boolean
17
+ onClick?: () => void
18
+ }
19
+
20
+ export interface ContextMenuProps {
21
+ x: number
22
+ y: number
23
+ entries: ContextMenuEntry[]
24
+ onClose: () => void
25
+ }
26
+
27
+ /** 菜单估算宽高(clamp 防越出视口,与 EditorView menuPos 常量对齐)。 */
28
+ const MENU_W = 224
29
+ const MENU_H = 176
30
+
31
+ /**
32
+ * 浮动右键菜单。
33
+ * @param props.x 视口 x 坐标
34
+ * @param props.y 视口 y 坐标
35
+ * @param props.entries 菜单项列表
36
+ * @param props.onClose 关闭回调(backdrop 点击/右键/Esc/点击项后触发)
37
+ */
38
+ export function ContextMenu(props: ContextMenuProps): React.ReactElement {
39
+ const { x, y, entries, onClose } = props
40
+
41
+ React.useEffect(() => {
42
+ const onKey = (e: KeyboardEvent): void => {
43
+ if (e.key === 'Escape') onClose()
44
+ }
45
+ window.addEventListener('keydown', onKey, true)
46
+ return () => window.removeEventListener('keydown', onKey, true)
47
+ }, [onClose])
48
+
49
+ const left = Math.max(4, Math.min(x, (window.innerWidth || 800) - MENU_W))
50
+ const top = Math.max(4, Math.min(y, (window.innerHeight || 600) - MENU_H))
51
+
52
+ const children: React.ReactNode[] = []
53
+ for (const entry of entries) {
54
+ if (entry.separator) {
55
+ children.push(React.createElement('div', { key: 'sep-' + entry.id, className: 'edrv-ctxmenu-sep' }))
56
+ continue
57
+ }
58
+ const cls = 'edrv-ctxmenu-item'
59
+ + (entry.danger ? ' edrv-ctxmenu-danger' : '')
60
+ + (entry.disabled ? ' edrv-ctxmenu-disabled' : '')
61
+ children.push(React.createElement('button', {
62
+ key: entry.id,
63
+ className: cls,
64
+ disabled: entry.disabled,
65
+ onClick: () => {
66
+ if (!entry.disabled) {
67
+ entry.onClick?.()
68
+ onClose()
69
+ }
70
+ },
71
+ }, entry.label))
72
+ }
73
+
74
+ return React.createElement(React.Fragment, null,
75
+ React.createElement('div', {
76
+ className: 'edrv-ctxmenu-backdrop',
77
+ style: { position: 'fixed', inset: 0, zIndex: 70 },
78
+ onClick: onClose,
79
+ onContextMenu: (e: React.MouseEvent) => { e.preventDefault(); onClose() },
80
+ }),
81
+ React.createElement('div', { className: 'edrv-ctxmenu', style: { left, top } }, ...children))
82
+ }
@@ -18,6 +18,7 @@ import { SidebarView } from '../sidebar/SidebarView.js'
18
18
  import { clearDiffDock, publishDiffDock } from '../diffDockStore.js'
19
19
  import { displayDiffTotal, editorDockMode } from '../diffDock.js'
20
20
  import { editorHeight } from '../editorLayout.js'
21
+ import { revealInExplorer as revealPathInExplorer } from '../fileReveal.js'
21
22
 
22
23
  /**
23
24
  * 中央编辑区:文件页签(脏点/关闭/打开路径)+ Ctrl+P 搜索 + Monaco 编辑器 +
@@ -491,6 +492,12 @@ export function EditorView(props) {
491
492
  if (s && p) menuHandlers()?.addRefToChat(p, { startLine: s.startLine, endLine: s.endLine })
492
493
  },
493
494
  })
495
+ // 在 OS 文件浏览器中打开/定位当前活动文件(与文件管理栏右键菜单同源能力)。
496
+ ed.addAction({
497
+ id: 'edrv.revealInExplorer', label: '在文件浏览器中打开', contextMenuGroupId: '1_edrv',
498
+ precondition: 'editorTextFocus',
499
+ run: (edx) => menuHandlers()?.openInExplorer(pathOf(edx)),
500
+ })
494
501
  // hover 差异块 → 浮出 Keep/Undo(req:鼠标移到编辑区差异块时显示)
495
502
  // 防闪烁:① 区域不变不 setState(浮窗锚定差异块起始行,不跟随鼠标);② 延迟隐藏;
496
503
  // ③ 浮窗自身 onMouseEnter 取消隐藏计时(鼠标在浮窗与编辑器间移动不闪)。
@@ -751,8 +758,23 @@ export function EditorView(props) {
751
758
  addToConversation.appendReference(sessionId, path, range).then((o) => setStatus(statusOfAdd(o, '已添加文件引用')))
752
759
  }
753
760
 
761
+ /**
762
+ * 在 OS 文件浏览器中打开/定位路径(Monaco 右键菜单用,状态栏反馈)。
763
+ * @author ddj 2026年08月27号
764
+ * @param path 工作区相对路径(可能为 null)
765
+ */
766
+ const openInExplorer = (path) => {
767
+ if (!path) { setStatus('无活动文件'); return }
768
+ if (!sessionId) { setStatus('无活动会话'); return }
769
+ setStatus('正在打开文件浏览器…')
770
+ revealPathInExplorer(sessionId, path).then((outcome) => {
771
+ setStatus(outcome.ok ? '已在文件浏览器中打开' : '打开失败')
772
+ if (!outcome.ok && outcome.error) setError(outcome.error)
773
+ })
774
+ }
775
+
754
776
  // 供 Monaco 原生右键菜单 addAction 读取的最新动作闭包(空依赖回调不随渲染重建)
755
- menuHandlersRef.current = { addRefToChat }
777
+ menuHandlersRef.current = { addRefToChat, openInExplorer }
756
778
 
757
779
  const openFile = (path, focusDiff) => {
758
780
  if (!path) return
@@ -949,6 +971,10 @@ export function EditorView(props) {
949
971
  pendingByPath,
950
972
  sum: { totalFiles: sum.totalFiles, files: sum.files },
951
973
  refreshRecords: () => refreshRecords(),
974
+ editor: () => editorRef.current,
975
+ outlineSources: props.outlineSources,
976
+ fileMenuItems: props.fileMenuItems,
977
+ notify: (message) => setStatus(message),
952
978
  }
953
979
 
954
980
  // 主编辑列(侧边栏右侧):pathBar + tabRow + 编辑/差异区(底部整条留给 DSH 对话输入栏)
package/src/compat.ts CHANGED
@@ -52,10 +52,12 @@ export function detectExternal(ctx: Ctx, depsAvailable: boolean): CompatAdapter[
52
52
  const mcpCount = entriesOf(ctx).length
53
53
  const settings = ctx.get('settings') as { describe?: unknown; update?: unknown } | undefined
54
54
  const hasSettings = Boolean(settings?.describe || settings?.update)
55
+ const sub = ctx.get('subprocess') as { spawn?: unknown } | undefined
55
56
  return [
56
57
  { name: MCP_PACKAGE, active: mcpCount > 0, note: mcpCount > 0 ? mcpCount + ' 个 MCP 服务条目' : '未检测到 MCP 条目(MCP 管理页显示为空)' },
57
58
  { name: '设置持久化(@deepseek-ai/dsh-settings)', active: depsAvailable, note: depsAvailable ? '设置 section 已安装' : '未安装:fileOpenTool 持久化降级为配置值' },
58
59
  { name: 'settings 服务', active: hasSettings, note: hasSettings ? '可读写设置' : '不可用(设置读写走配置回退)' },
60
+ { name: '文件浏览器打开(subprocess 服务)', active: typeof sub?.spawn === 'function', note: typeof sub?.spawn === 'function' ? '可定位/打开 OS 文件浏览器' : '不可用(右键「在文件浏览器中打开」将提示失败)' },
59
61
  ]
60
62
  }
61
63