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.
- package/README.md +17 -7
- package/lib/client.js +1649 -272
- package/lib/client.js.map +1 -1
- package/lib/index.js +87 -0
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/diffDock.ts +52 -0
- package/src/client/diffDockStore.ts +87 -0
- package/src/client/editorLayout.ts +19 -0
- package/src/client/events.ts +40 -11
- package/src/client/index.ts +49 -13
- 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/SidebarView.ts +73 -0
- package/src/client/sidebar/panels/FileExplorer.ts +161 -0
- package/src/client/sidebar/panels/index.ts +24 -0
- package/src/client/sidebar/registry.ts +52 -0
- package/src/client/sidebar/types.ts +36 -0
- package/src/client/styles/editor.css +130 -5
- package/src/client/ui/ConversationDiffDock.ts +84 -0
- package/src/client/ui/DiffBox.ts +78 -29
- package/src/client/ui/EditorView.ts +209 -52
- package/src/rpc.ts +21 -0
- package/src/shared/rpc.ts +10 -0
- package/src/tree.ts +62 -0
|
@@ -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
|
+
}
|