dsh-vscode-mode 0.1.19 → 0.1.21

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,271 @@
1
+ /**
2
+ * dsh-vscode-mode client — 大纲兜底解析器(纯函数,无 DOM/Monaco 依赖,node 可测)。
3
+ * 覆盖无内置 document symbol provider 的语言:markdown/mdx、python、shell、powershell、
4
+ * lua、go、rust、yaml、ini(toml)、花括号语言(c/cpp/csharp/java/php/ruby/kotlin/swift/dart)。
5
+ * TS/JS/JSON/CSS/HTML 有 Monaco 原生 provider,不走这里。
6
+ * 作者 ddj 2026-08-27
7
+ */
8
+ import type { OutlineSymbol } from './types.js'
9
+
10
+ /** monaco SymbolKind 数值常量(与 LSP SymbolKind 一致,面板渲染按此分组)。 */
11
+ export const SK = {
12
+ File: 0, Module: 1, Namespace: 2, Package: 3, Class: 4, Method: 5,
13
+ Property: 6, Field: 7, Constructor: 8, Enum: 9, Interface: 10,
14
+ Function: 11, Variable: 12, Constant: 13, String: 14, Number: 15,
15
+ Boolean: 16, Array: 17, Object: 18, Key: 19, Null: 20, EnumMember: 21,
16
+ Struct: 22, Event: 23, Operator: 24, TypeParameter: 25,
17
+ } as const
18
+
19
+ /** 花括号语言的泛型函数名排除集(避免误把控制流/关键字当符号)。 */
20
+ const BRACE_KW = new Set([
21
+ 'if', 'for', 'while', 'switch', 'catch', 'foreach', 'using', 'with', 'when',
22
+ 'match', 'return', 'new', 'in', 'of', 'do', 'else', 'elif', 'then', 'yield',
23
+ 'await', 'typeof', 'instanceof', 'case', 'default', 'try', 'finally', 'throw',
24
+ ])
25
+
26
+ /** 花括号语言的类型关键字 → SymbolKind。 */
27
+ const BRACE_TYPE_KIND: Record<string, number> = {
28
+ class: SK.Class, struct: SK.Struct, interface: SK.Interface, enum: SK.Enum,
29
+ namespace: SK.Namespace, module: SK.Module, trait: SK.Interface, record: SK.Class,
30
+ }
31
+
32
+ /** 按文档序(先序)补齐每个符号的 endLine:叶子取“下一个符号起行 - 1”,容器取末子 endLine。 */
33
+ function computeEnds(symbols: OutlineSymbol[], lastLine: number): void {
34
+ const order: OutlineSymbol[] = []
35
+ const collect = (list: OutlineSymbol[]): void => {
36
+ for (const s of list) {
37
+ order.push(s)
38
+ if (s.children && s.children.length) collect(s.children)
39
+ }
40
+ }
41
+ collect(symbols)
42
+ for (let i = order.length - 1; i >= 0; i--) {
43
+ const s = order[i]
44
+ const kids = s.children ?? []
45
+ if (kids.length) {
46
+ s.endLine = Math.max(s.startLine, kids[kids.length - 1].endLine)
47
+ } else {
48
+ const next = order[i + 1]
49
+ const end = next ? Math.max(s.startLine, next.startLine - 1) : lastLine
50
+ s.endLine = Math.max(s.startLine, Math.min(end, lastLine))
51
+ }
52
+ }
53
+ }
54
+
55
+ /** 构造单行符号(起始行=跳转行=所在行;detail 为截断后的声明文本)。 */
56
+ function mk(lineIndex: number, name: string, kind: number, raw: string): OutlineSymbol {
57
+ const detail = raw.trim().replace(/\s+/g, ' ').slice(0, 80)
58
+ const line = lineIndex + 1
59
+ const out: OutlineSymbol = { name, kind, startLine: line, endLine: line, selectLine: line }
60
+ if (detail && detail !== name) out.detail = detail
61
+ return out
62
+ }
63
+
64
+ /** Markdown/mdx:`#` 级标题,层级嵌套。 */
65
+ function parseMarkdown(text: string): OutlineSymbol[] {
66
+ const roots: OutlineSymbol[] = []
67
+ const stack: Array<{ level: number; item: OutlineSymbol }> = []
68
+ const lines = text.split('\n')
69
+ for (let i = 0; i < lines.length; i++) {
70
+ const m = /^(\#{1,6})\s+(.+?)\s*#*\s*$/.exec(lines[i])
71
+ if (!m) continue
72
+ const item: OutlineSymbol = {
73
+ name: m[2].trim(), kind: SK.Namespace,
74
+ startLine: i + 1, endLine: i + 1, selectLine: i + 1, children: [],
75
+ }
76
+ while (stack.length && stack[stack.length - 1].level >= m[1].length) stack.pop()
77
+ const parent = stack.length ? stack[stack.length - 1].item : null
78
+ ;(parent ? parent.children : roots)!.push(item)
79
+ stack.push({ level: m[1].length, item })
80
+ }
81
+ computeEnds(roots, lines.length)
82
+ return roots
83
+ }
84
+
85
+ /** Python:def/class 按缩进栈嵌套。 */
86
+ function parsePython(text: string): OutlineSymbol[] {
87
+ const roots: OutlineSymbol[] = []
88
+ const stack: Array<{ indent: number; item: OutlineSymbol }> = []
89
+ const lines = text.split('\n')
90
+ for (let i = 0; i < lines.length; i++) {
91
+ const m = /^([ \t]*)(?:class|def)\s+([A-Za-z_]\w*)\s*[(:]/.exec(lines[i])
92
+ if (!m) continue
93
+ const indent = m[1].replace(/\t/g, ' ').length
94
+ const isClass = /class\s/.test(lines[i])
95
+ const item: OutlineSymbol = {
96
+ name: m[2], kind: isClass ? SK.Class : SK.Function,
97
+ startLine: i + 1, endLine: i + 1, selectLine: i + 1, children: [],
98
+ }
99
+ while (stack.length && stack[stack.length - 1].indent >= indent) stack.pop()
100
+ const parent = stack.length ? stack[stack.length - 1].item : null
101
+ ;(parent ? parent.children : roots)!.push(item)
102
+ stack.push({ indent, item })
103
+ }
104
+ computeEnds(roots, lines.length)
105
+ return roots
106
+ }
107
+
108
+ /** Shell:`name() {` 函数。 */
109
+ function parseShell(text: string): OutlineSymbol[] {
110
+ const out: OutlineSymbol[] = []
111
+ const lines = text.split('\n')
112
+ for (let i = 0; i < lines.length; i++) {
113
+ const m = /^[ \t]*([A-Za-z_][A-Za-z0-9_]*)[ \t]*\(\)[ \t]*\{/.exec(lines[i])
114
+ if (m) out.push(mk(i, m[1], SK.Function, lines[i]))
115
+ }
116
+ computeEnds(out, lines.length)
117
+ return out
118
+ }
119
+
120
+ /** PowerShell:`function Name {` / `filter Name {`。 */
121
+ function parsePwsh(text: string): OutlineSymbol[] {
122
+ const out: OutlineSymbol[] = []
123
+ const lines = text.split('\n')
124
+ for (let i = 0; i < lines.length; i++) {
125
+ const m = /^[ \t]*(?:function|filter)[ \t]+([\w.-]+)/.exec(lines[i])
126
+ if (m) out.push(mk(i, m[1], SK.Function, lines[i]))
127
+ }
128
+ computeEnds(out, lines.length)
129
+ return out
130
+ }
131
+
132
+ /** Lua:`function Foo:bar` / `M.bar = class|function`。 */
133
+ function parseLua(text: string): OutlineSymbol[] {
134
+ const out: OutlineSymbol[] = []
135
+ const lines = text.split('\n')
136
+ for (let i = 0; i < lines.length; i++) {
137
+ const line = lines[i]
138
+ if (/^\s*--/.test(line)) continue
139
+ const fm = /^[ \t]*(?:local[ \t]+)?function[ \t]+([A-Za-z_][\w.:]*)/.exec(line)
140
+ if (fm) { out.push(mk(i, fm[1], SK.Function, line)); continue }
141
+ const cm = /^[ \t]*([A-Za-z_][\w.]*)[ \t]*=[ \t]*(?:class|function)\b/.exec(line)
142
+ if (cm) out.push(mk(i, cm[1], /class\b/.test(line) ? SK.Class : SK.Function, line))
143
+ }
144
+ computeEnds(out, lines.length)
145
+ return out
146
+ }
147
+
148
+ /** Go:func(含 receiver)/ type。 */
149
+ function parseGo(text: string): OutlineSymbol[] {
150
+ const out: OutlineSymbol[] = []
151
+ const lines = text.split('\n')
152
+ for (let i = 0; i < lines.length; i++) {
153
+ const fm = /^[ \t]*func[ \t]+(?:\([^)]*\)[ \t]*)?([A-Za-z_]\w*)/.exec(lines[i])
154
+ if (fm) { out.push(mk(i, fm[1], SK.Function, lines[i])); continue }
155
+ const tm = /^[ \t]*type[ \t]+([A-Za-z_]\w*)/.exec(lines[i])
156
+ if (tm) out.push(mk(i, tm[1], SK.Struct, lines[i]))
157
+ }
158
+ computeEnds(out, lines.length)
159
+ return out
160
+ }
161
+
162
+ /** Rust:fn/struct/enum/trait/impl/mod/type。 */
163
+ function parseRust(text: string): OutlineSymbol[] {
164
+ const out: OutlineSymbol[] = []
165
+ const lines = text.split('\n')
166
+ const kindOf: Record<string, number> = {
167
+ fn: SK.Function, struct: SK.Struct, enum: SK.Enum, trait: SK.Interface,
168
+ impl: SK.Module, mod: SK.Module, type: SK.TypeParameter,
169
+ }
170
+ for (let i = 0; i < lines.length; i++) {
171
+ const m = /^[ \t]*(?:pub(?:\([^)]*\))?[ \t]+)?(fn|struct|enum|trait|impl|mod|type)[ \t]+([A-Za-z_]\w*)/.exec(lines[i])
172
+ if (m && kindOf[m[1]]) out.push(mk(i, m[2], kindOf[m[1]], lines[i]))
173
+ }
174
+ computeEnds(out, lines.length)
175
+ return out
176
+ }
177
+
178
+ /** YAML:仅顶层键。 */
179
+ function parseYaml(text: string): OutlineSymbol[] {
180
+ const out: OutlineSymbol[] = []
181
+ const lines = text.split('\n')
182
+ for (let i = 0; i < lines.length; i++) {
183
+ const line = lines[i]
184
+ if (!line.trim() || /^\s/.test(line) || /^\s*[#-]/.test(line)) continue
185
+ const m = /^([A-Za-z0-9_.\-]+)\s*:/.exec(line)
186
+ if (m) out.push(mk(i, m[1], SK.Key, line))
187
+ }
188
+ computeEnds(out, lines.length)
189
+ return out
190
+ }
191
+
192
+ /** INI/TOML:`[section]` + `key=value`。 */
193
+ function parseIni(text: string): OutlineSymbol[] {
194
+ const out: OutlineSymbol[] = []
195
+ const lines = text.split('\n')
196
+ for (let i = 0; i < lines.length; i++) {
197
+ const line = lines[i]
198
+ if (!line.trim() || /^\s*[;#]/.test(line)) continue
199
+ const sm = /^\[([^\]]+)\]/.exec(line)
200
+ if (sm) { out.push(mk(i, sm[1], SK.Namespace, line)); continue }
201
+ const km = /^([A-Za-z0-9_.\-]+)\s*[=:]/.exec(line.trim())
202
+ if (km) out.push(mk(i, km[1], SK.Key, line))
203
+ }
204
+ computeEnds(out, lines.length)
205
+ return out
206
+ }
207
+
208
+ /** 花括号语言(C 族/JVM/.NET):类型声明 + 泛型函数扫描(扁平,best-effort)。 */
209
+ function parseBrace(text: string): OutlineSymbol[] {
210
+ const out: OutlineSymbol[] = []
211
+ const lines = text.split('\n')
212
+ const typeRe = /^[ \t]*(?:(?:public|private|protected|internal|static|final|abstract|sealed|readonly|export|declare|async|open|extern|virtual|override|const|global|pub|local)\s+)*(class|struct|interface|enum|namespace|module|trait|record)\s+([A-Za-z_]\w*)/
213
+ // 泛型函数:允许任意“字词型”返回类型/修饰符前缀(含 :: 限定名),名字后跟 ( 且不以 ; 结尾
214
+ const funcRe = /^[ \t]*(?:[\w<>,*&\[\].:]+\s+)*([A-Za-z_]\w*)\s*(?:<[^>]*>)?\s*\(/
215
+ for (let i = 0; i < lines.length; i++) {
216
+ const line = lines[i]
217
+ const t = line.trim()
218
+ if (!t || /^\s*\*/.test(line)) continue
219
+ const tm = typeRe.exec(line)
220
+ if (tm) { out.push(mk(i, tm[2], BRACE_TYPE_KIND[tm[1]] ?? SK.Class, line)); continue }
221
+ const fm = funcRe.exec(line)
222
+ if (!fm) continue
223
+ if (BRACE_KW.has(fm[1]) || /;\s*$/.test(t)) continue
224
+ out.push(mk(i, fm[1], SK.Function, line))
225
+ }
226
+ computeEnds(out, lines.length)
227
+ return out
228
+ }
229
+
230
+ /**
231
+ * 按 Monaco languageId 解析大纲符号(纯函数)。
232
+ * @author ddj 2026年08月27号
233
+ * @param languageId Monaco 语言 id
234
+ * @param text 文件全文
235
+ * @returns 归一化大纲符号树
236
+ */
237
+ export function parseOutline(languageId: string, text: string): OutlineSymbol[] {
238
+ switch (languageId) {
239
+ case 'markdown':
240
+ case 'mdx':
241
+ return parseMarkdown(text)
242
+ case 'python':
243
+ return parsePython(text)
244
+ case 'shell':
245
+ return parseShell(text)
246
+ case 'powershell':
247
+ return parsePwsh(text)
248
+ case 'lua':
249
+ return parseLua(text)
250
+ case 'go':
251
+ return parseGo(text)
252
+ case 'rust':
253
+ return parseRust(text)
254
+ case 'yaml':
255
+ return parseYaml(text)
256
+ case 'ini':
257
+ return parseIni(text)
258
+ case 'c':
259
+ case 'cpp':
260
+ case 'csharp':
261
+ case 'java':
262
+ case 'php':
263
+ case 'ruby':
264
+ case 'kotlin':
265
+ case 'swift':
266
+ case 'dart':
267
+ return parseBrace(text)
268
+ default:
269
+ return []
270
+ }
271
+ }
@@ -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,73 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — 侧边栏容器(活动栏 + 面板区 + 拖拽调宽)。
4
+ * 面板列表来自注册表(活动栏图标/徽标/激活态),渲染当前激活面板的 render(ctx);
5
+ * 右侧手柄拖拽调宽(clamp 180–560),底部按钮收起侧边栏。
6
+ * 作者 ddj 2026-08-26
7
+ */
8
+ import React from 'react'
9
+ import type { SidebarCtx } from './types.js'
10
+
11
+ const W_MIN = 180
12
+ const W_MAX = 560
13
+
14
+ /**
15
+ * 侧边栏容器。
16
+ * @param props.registry 面板注册表(list() 提供面板顺序)
17
+ * @param props.ctx 面板共享上下文
18
+ * @param props.activePanel 当前激活面板 id
19
+ * @param props.onActive 激活面板变化回调
20
+ * @param props.width 面板区宽度
21
+ * @param props.onWidth 宽度变化回调
22
+ * @param props.onHide 收起侧边栏回调
23
+ */
24
+ export function SidebarView(props) {
25
+ const panels = props.registry?.list?.() ?? []
26
+ const ctx = props.ctx
27
+ const activePanel = props.activePanel
28
+ const onActive = props.onActive
29
+ const width = props.width
30
+ const onWidth = props.onWidth
31
+ const onHide = props.onHide
32
+
33
+ const active = panels.find((p) => p.id === activePanel) || panels[0]
34
+
35
+ const onPointerDown = (e) => {
36
+ e.preventDefault()
37
+ const startX = e.clientX
38
+ const startW = width
39
+ const onMove = (ev) => {
40
+ const w = Math.max(W_MIN, Math.min(W_MAX, startW + (ev.clientX - startX)))
41
+ onWidth(w)
42
+ }
43
+ const onUp = () => {
44
+ window.removeEventListener('pointermove', onMove)
45
+ window.removeEventListener('pointerup', onUp)
46
+ }
47
+ window.addEventListener('pointermove', onMove)
48
+ window.addEventListener('pointerup', onUp)
49
+ }
50
+
51
+ const rail = React.createElement('div', { className: 'edrv-rail' },
52
+ React.createElement('div', { style: { flex: 1, display: 'flex', flexDirection: 'column', gap: 2 } },
53
+ panels.map((p) => {
54
+ const count = typeof p.badge === 'function' ? p.badge(ctx) : null
55
+ return React.createElement('button', {
56
+ key: p.id,
57
+ className: 'edrv-rail-btn' + (active && p.id === active.id ? ' edrv-rail-on' : ''),
58
+ title: p.title,
59
+ onClick: () => onActive(p.id),
60
+ },
61
+ React.createElement('span', { className: 'edrv-rail-icon' }, p.icon),
62
+ count > 0
63
+ ? React.createElement('span', { className: 'edrv-rail-badge' }, String(count))
64
+ : null)
65
+ })),
66
+ React.createElement('button', { className: 'edrv-rail-btn', title: '隐藏侧边栏 (Ctrl+B)', onClick: onHide }, '◀'))
67
+
68
+ return React.createElement('div', { className: 'edrv-sidebar', style: { width } },
69
+ rail,
70
+ React.createElement('div', { className: 'edrv-side-body' },
71
+ active ? active.render(ctx) : null),
72
+ React.createElement('div', { className: 'edrv-side-resize', onPointerDown: onPointerDown }))
73
+ }
@@ -0,0 +1,161 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — 侧边栏「文件管理」面板(懒加载目录树)。
4
+ * 点目录展开/收起(首次经 edrv.listDir 拉取并缓存),点文件经 ctx.openFile 打开;
5
+ * 差异角标取 pendingByPath,活动文件高亮;edrv:refresh 事件与手动刷新重载树。
6
+ * 作者 ddj 2026-08-26
7
+ */
8
+ import React from 'react'
9
+ import { rpc } from '../../rpc.js'
10
+ import type { SidebarCtx } from '../types.js'
11
+
12
+ const DIR_CAP = 4000
13
+
14
+ /**
15
+ * 目录树面板主体。
16
+ * @param props.ctx 面板共享上下文(sessionId/openFile/activePath/pendingByPath)
17
+ */
18
+ export function FileExplorer(props) {
19
+ const ctx = props?.ctx
20
+ const sessionId = ctx?.sessionId
21
+ const openFile = ctx?.openFile
22
+ const activePath = ctx?.activePath ?? null
23
+ const pendingByPath = ctx?.pendingByPath ?? {}
24
+ const [root, setRoot] = React.useState(null)
25
+ const [dirs, setDirs] = React.useState({})
26
+ const [expanded, setExpanded] = React.useState({})
27
+ const [loading, setLoading] = React.useState({})
28
+ const [error, setError] = React.useState(null)
29
+ const tokensRef = React.useRef({})
30
+ // expanded 的 ref 镜像:edrv:refresh 监听用首次渲染闭包,但需读到最新展开态
31
+ const expandedRef = React.useRef({})
32
+ expandedRef.current = expanded
33
+
34
+ const loadDir = (rel) => {
35
+ const token = (tokensRef.current[rel] || 0) + 1
36
+ tokensRef.current[rel] = token
37
+ setLoading((prev) => Object.assign({}, prev, { [rel]: true }))
38
+ setError(null)
39
+ rpc('edrv.listDir', { sessionId, path: rel }).then((res) => {
40
+ if (tokensRef.current[rel] !== token) return
41
+ if (res && res.ok && Array.isArray(res.entries)) {
42
+ setDirs((prev) => Object.assign({}, prev, { [rel]: res.entries }))
43
+ if (res.root && !root) setRoot(res.root)
44
+ } else {
45
+ setError(res?.error ? String(res.error) : '读取目录失败')
46
+ }
47
+ }).catch((e) => {
48
+ if (tokensRef.current[rel] !== token) return
49
+ setError('读取目录异常:' + String(e))
50
+ }).finally(() => {
51
+ if (tokensRef.current[rel] !== token) return
52
+ setLoading((prev) => {
53
+ const next = Object.assign({}, prev)
54
+ delete next[rel]
55
+ return next
56
+ })
57
+ })
58
+ }
59
+
60
+ const toggle = (rel) => {
61
+ if (expanded[rel] === true) {
62
+ setExpanded((prev) => Object.assign({}, prev, { [rel]: false }))
63
+ return
64
+ }
65
+ if (!dirs[rel]) void loadDir(rel)
66
+ setExpanded((prev) => Object.assign({}, prev, { [rel]: true }))
67
+ }
68
+
69
+ const refresh = () => {
70
+ const keep = Object.keys(expandedRef.current).filter((k) => expandedRef.current[k] === true)
71
+ tokensRef.current = {}
72
+ setDirs({})
73
+ setLoading({})
74
+ setError(null)
75
+ if (keep.length) {
76
+ const next = {}
77
+ for (const rel of keep) next[rel] = true
78
+ setExpanded(next)
79
+ for (const rel of keep) void loadDir(rel)
80
+ } else {
81
+ setExpanded({})
82
+ }
83
+ void loadDir('')
84
+ }
85
+
86
+ React.useEffect(() => {
87
+ tokensRef.current = {}
88
+ setDirs({})
89
+ setExpanded({})
90
+ setLoading({})
91
+ setError(null)
92
+ setRoot(null)
93
+ void loadDir('')
94
+ // eslint-disable-next-line react-hooks/exhaustive-deps
95
+ }, [sessionId])
96
+
97
+ React.useEffect(() => {
98
+ const onRefresh = () => refresh()
99
+ window.addEventListener('edrv:refresh', onRefresh)
100
+ return () => window.removeEventListener('edrv:refresh', onRefresh)
101
+ // eslint-disable-next-line react-hooks/exhaustive-deps
102
+ }, [])
103
+
104
+ const rootName = root ? String(root).split(/[\\/]/).pop() || root : ''
105
+
106
+ const rowEl = (e, depth, isDir, isOpen) => {
107
+ const pending = isDir ? 0 : (pendingByPath[e.path] ?? 0)
108
+ const active = !isDir && e.path === activePath
109
+ const dim = e.type === 'other'
110
+ return React.createElement('div', {
111
+ key: e.path,
112
+ className: 'edrv-tree-row' + (active ? ' edrv-tree-active' : ''),
113
+ title: e.path,
114
+ style: { paddingLeft: 6 + depth * 14 },
115
+ onClick: () => { if (isDir) toggle(e.path); else openFile(e.path) },
116
+ },
117
+ React.createElement('span', { className: 'edrv-tree-chev' },
118
+ isDir ? (isOpen ? '▾' : '▸') : ''),
119
+ React.createElement('span', { className: 'edrv-tree-name' + (dim ? ' edrv-tree-dim' : '') },
120
+ isDir ? (isOpen ? '📂' : '📁') : '📄', ' ', e.name),
121
+ (pending > 0
122
+ ? React.createElement('span', { className: 'edrv-tree-badge' }, String(pending))
123
+ : null))
124
+ }
125
+
126
+ const rowsOf = (rel, depth) => {
127
+ const entries = dirs[rel]
128
+ if (!entries) {
129
+ return [React.createElement('div', { key: rel + ':loading', className: 'edrv-tree-loading', style: { paddingLeft: 6 + depth * 14 } },
130
+ loading[rel] ? '加载中…' : '无法加载')]
131
+ }
132
+ const rows = []
133
+ const cap = entries.length
134
+ for (let i = 0; i < cap; i++) {
135
+ const e = entries[i]
136
+ const isDir = e.type === 'directory'
137
+ const isOpen = expanded[e.path] === true
138
+ rows.push(rowEl(e, depth, isDir, isOpen))
139
+ if (isDir && isOpen) rows.push(...rowsOf(e.path, depth + 1))
140
+ }
141
+ if (cap >= DIR_CAP) {
142
+ rows.push(React.createElement('div', { key: rel + ':cap', className: 'edrv-tree-loading', style: { paddingLeft: 6 + depth * 14 } },
143
+ '目录条目过多,仅显示前 ' + DIR_CAP + ' 项'))
144
+ }
145
+ return rows
146
+ }
147
+
148
+ return React.createElement('div', { className: 'edrv-side-panel' },
149
+ React.createElement('div', { className: 'edrv-side-head' },
150
+ React.createElement('span', { className: 'edrv-side-title' }, '资源管理器'),
151
+ React.createElement('span', { className: 'edrv-side-root', title: root || '' }, rootName),
152
+ React.createElement('span', { style: { flex: 1 } }),
153
+ React.createElement('button', { className: 'edrv-side-btn', title: '刷新目录树', onClick: refresh }, '⟳')),
154
+ React.createElement('div', { className: 'edrv-tree' },
155
+ (error
156
+ ? React.createElement('div', { className: 'edrv-tree-error' },
157
+ React.createElement('span', null, String(error)),
158
+ React.createElement('button', { className: 'edrv-side-btn', onClick: () => refresh() }, '重试'))
159
+ : null),
160
+ rowsOf('', 0)))
161
+ }