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.
- package/lib/client.js +1009 -8
- package/lib/client.js.map +1 -1
- package/lib/index.js +113 -0
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/fileReveal.ts +28 -0
- package/src/client/index.ts +16 -1
- package/src/client/outline/OutlinePanel.tsx +213 -0
- package/src/client/outline/index.ts +24 -0
- package/src/client/outline/parse.ts +271 -0
- package/src/client/outline/sources.ts +134 -0
- package/src/client/outline/types.ts +51 -0
- package/src/client/sidebar/contextMenu.ts +99 -0
- package/src/client/sidebar/menuItems.ts +28 -0
- package/src/client/sidebar/panels/FileExplorer.ts +34 -4
- package/src/client/sidebar/types.ts +10 -0
- package/src/client/styles/editor.css +12 -1
- package/src/client/ui/ContextMenu.ts +82 -0
- package/src/client/ui/EditorView.ts +27 -1
- package/src/compat.ts +2 -0
- package/src/reveal.ts +64 -0
- package/src/rpc.ts +21 -0
- package/src/shared/rpc.ts +2 -0
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
|