dsh-vscode-mode 0.1.20 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 中央编辑器(文件页签/QuickOpen/状态栏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚),状态持久化到工作区旁车",
5
5
  "keywords": [
6
6
  "dsh",
@@ -28,6 +28,8 @@ import { SIDEBAR_PLUGIN, pickSettingsBinder, registerSlotSafely } from './compat
28
28
  import { createAddToConversation } from './addToConversation.js'
29
29
  import { createSidebarPanelRegistry } from './sidebar/registry.js'
30
30
  import { createFilePanel } from './sidebar/panels/index.js'
31
+ import { createOutlinePanel } from './outline/index.js'
32
+ import { createOutlineSourceRegistry, registerBuiltinOutlineSources } from './outline/sources.js'
31
33
  import type { CompatAdapter } from '../shared/compat.js'
32
34
 
33
35
  export const inject = ['slots', 'timer', 'locale', 'connection', 'remote', 'workspaces', 'sessions', 'conversation', 'settingsScope', 'webUiSettings']
@@ -85,6 +87,11 @@ export function apply(ctx: any): void {
85
87
  const sidebarPanels = createSidebarPanelRegistry()
86
88
  ctx.provide('edrvSidebarPanels', sidebarPanels)
87
89
  ctx.effect(() => sidebarPanels.register(createFilePanel()), 'vscode-mode: sidebar panel')
90
+ // 大纲源注册表(公开预留口):第三方语言插件(LSP/VSIX 等)注册更高优先级源即可覆盖兜底
91
+ const outlineSources = createOutlineSourceRegistry()
92
+ ctx.provide('edrvOutlineSources', outlineSources)
93
+ ctx.effect(() => registerBuiltinOutlineSources(outlineSources), 'vscode-mode: outline sources')
94
+ ctx.effect(() => sidebarPanels.register(createOutlinePanel()), 'vscode-mode: sidebar panel outline')
88
95
  ctx.effect(() => registry.register({
89
96
  id: 'system', label: '系统默认应用', priority: 0,
90
97
  open: (path: string) => originalOpenPath.call(workspaces, path),
@@ -126,7 +133,7 @@ export function apply(ctx: any): void {
126
133
  order: 5,
127
134
  label: '文件编辑',
128
135
  inject: (sessionId: string) => ({ sessionId }),
129
- }, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { schedule, addToConversation, sidebarPanels })))
136
+ }, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources })))
130
137
 
131
138
  // 对话输入框上方差异 dock:普通对话显示单文案按钮,文件编辑页由 EditorView 隐藏
132
139
  registerSlotSafely(ctx, {
@@ -0,0 +1,213 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — 侧边栏「大纲」面板。
4
+ * 数据源经 resolveOutline(源注册表,优先级降序、首个非空生效)解析当前活动文件符号:
5
+ * monaco 源吃原生 document symbol provider,fallback 源兜底无内置提供方的语言。
6
+ * 交互:点击符号跳转编辑器、▸/▾ 折叠、光标所在符号高亮、空/加载/错误态。
7
+ * 作者 ddj 2026-08-27
8
+ */
9
+ import React from 'react'
10
+ import { resolveOutline } from './sources.js'
11
+ import type { SidebarCtx } from '../sidebar/types.js'
12
+
13
+ /** 渲染符号数上限(防超大文件卡顿,超出显示截断提示)。 */
14
+ const RENDER_CAP = 800
15
+
16
+ /** kind 分组元信息(字形 + 样式类)。 */
17
+ const KIND_GROUPS = {
18
+ func: { glyph: 'ƒ', cls: 'edrv-ol-func' },
19
+ type: { glyph: 'C', cls: 'edrv-ol-type' },
20
+ data: { glyph: '•', cls: 'edrv-ol-data' },
21
+ ns: { glyph: '▤', cls: 'edrv-ol-ns' },
22
+ key: { glyph: '·', cls: 'edrv-ol-key' },
23
+ }
24
+
25
+ /** SymbolKind 数值 → 分组(File0..Package3=ns;Class4/Enum9/Interface10/Struct22/TypeParameter25=type;Method5/Constructor8/Function11=func;Key19/EnumMember21=key)。 */
26
+ function kindMeta(kind) {
27
+ const k = kind | 0
28
+ if (k <= 3) return KIND_GROUPS.ns
29
+ if (k === 4 || k === 9 || k === 10 || k === 22 || k === 25) return KIND_GROUPS.type
30
+ if (k === 5 || k === 8 || k === 11) return KIND_GROUPS.func
31
+ if (k === 19 || k === 21) return KIND_GROUPS.key
32
+ return KIND_GROUPS.data
33
+ }
34
+
35
+ /**
36
+ * 大纲面板主体。
37
+ * @param props.ctx 面板共享上下文(editor/outlineSources/activePath)
38
+ */
39
+ export function OutlinePanel(props) {
40
+ const ctx = props?.ctx
41
+ const activePath = ctx?.activePath ?? null
42
+ const [symbols, setSymbols] = React.useState(null)
43
+ const [error, setError] = React.useState(null)
44
+ const [collapsed, setCollapsed] = React.useState({})
45
+ const [cursorLine, setCursorLine] = React.useState(null)
46
+ const seqRef = React.useRef(0)
47
+ // ref 镜像:refresh 稳定([]),监听器只挂一次,避免 EditorView 每渲染重建 ctx 造成抖动
48
+ const ctxRef = React.useRef(ctx)
49
+ ctxRef.current = ctx
50
+ const activeRef = React.useRef(activePath)
51
+ activeRef.current = activePath
52
+ const sourcesRef = React.useRef(ctx?.outlineSources)
53
+ sourcesRef.current = ctx?.outlineSources
54
+
55
+ const refresh = React.useCallback(() => {
56
+ const c = ctxRef.current
57
+ const ed = c?.editor?.()
58
+ const model = ed?.getModel?.()
59
+ const seq = ++seqRef.current
60
+ if (!ed || !model) { setSymbols(null); setError(null); return }
61
+ const uriPath = model?.uri?.path
62
+ const modelPath = uriPath ? decodeURIComponent(String(uriPath).replace(/^\//, '')) : null
63
+ const active = activeRef.current
64
+ if (active && modelPath !== active) { setSymbols(null); setError(null); return }
65
+ const sources = sourcesRef.current
66
+ if (!sources) { setSymbols([]); setError(null); return }
67
+ setError(null)
68
+ resolveOutline(sources, { languageId: model.getLanguageId(), model, editor: ed, monaco: window.monaco })
69
+ .then((list) => { if (seq === seqRef.current) setSymbols(list || []) })
70
+ .catch((e) => { if (seq === seqRef.current) { setError(String(e?.message ?? e)); setSymbols(null) } })
71
+ }, [])
72
+
73
+ // 编辑器监听:模型切换/内容编辑(防抖)/光标移动 + edrv:refresh;
74
+ // 编辑器未就绪时轮询等待(避免面板先于 Monaco 挂载后永久停在加载态)
75
+ React.useEffect(() => {
76
+ const disposers = []
77
+ let debounceTimer = null
78
+ let waitTimer = null
79
+ const attach = () => {
80
+ const ed = ctxRef.current?.editor?.()
81
+ if (!ed) return false
82
+ let contentDisposable = null
83
+ const subContent = (model) => {
84
+ if (contentDisposable) { contentDisposable.dispose(); contentDisposable = null }
85
+ contentDisposable = model?.onDidChangeContent?.(() => {
86
+ if (debounceTimer) clearTimeout(debounceTimer)
87
+ debounceTimer = setTimeout(() => { debounceTimer = null; refresh() }, 300)
88
+ }) ?? null
89
+ }
90
+ const onModel = () => { subContent(ed.getModel?.()); refresh() }
91
+ const onCursor = (e) => setCursorLine(e?.position?.lineNumber ?? null)
92
+ subContent(ed.getModel?.())
93
+ const subs = [
94
+ ed.onDidChangeModel?.(onModel),
95
+ ed.onDidChangeCursorPosition?.(onCursor),
96
+ { dispose: () => { if (contentDisposable) contentDisposable.dispose() } },
97
+ ]
98
+ for (const s of subs) if (s) disposers.push(s)
99
+ refresh()
100
+ return true
101
+ }
102
+ const onRefresh = () => refresh()
103
+ window.addEventListener('edrv:refresh', onRefresh)
104
+ if (!attach()) {
105
+ waitTimer = setInterval(() => { if (attach()) { clearInterval(waitTimer); waitTimer = null } }, 400)
106
+ }
107
+ return () => {
108
+ if (debounceTimer) clearTimeout(debounceTimer)
109
+ if (waitTimer) clearInterval(waitTimer)
110
+ for (const d of disposers) if (d?.dispose) d.dispose()
111
+ window.removeEventListener('edrv:refresh', onRefresh)
112
+ }
113
+ }, [refresh])
114
+
115
+ // 活动文件变化 → 重拉
116
+ React.useEffect(() => { refresh() }, [refresh, activePath])
117
+
118
+ const jump = (sym) => {
119
+ const ed = ctxRef.current?.editor?.()
120
+ if (!ed) return
121
+ const line = Math.max(1, sym?.selectLine ?? sym?.startLine ?? 1)
122
+ ed.revealLineInCenter(line)
123
+ ed.setPosition({ lineNumber: line, column: 1 })
124
+ ed.focus()
125
+ }
126
+
127
+ const toggleCollapse = (key) => {
128
+ setCollapsed((prev) => Object.assign({}, prev, { [key]: prev[key] === true ? false : true }))
129
+ }
130
+
131
+ const allKeys = React.useMemo(() => {
132
+ const keys = []
133
+ const walk = (list, prefix) => {
134
+ for (let i = 0; i < list.length; i++) {
135
+ const key = prefix ? prefix + '/' + i : String(i)
136
+ const sym = list[i]
137
+ if (sym.children && sym.children.length) { keys.push(key); walk(sym.children, key) }
138
+ }
139
+ }
140
+ walk(symbols || [], '')
141
+ return keys
142
+ }, [symbols])
143
+
144
+ const setAll = (value) => {
145
+ const next = {}
146
+ for (const key of allKeys) next[key] = value
147
+ setCollapsed(next)
148
+ }
149
+
150
+ const renderTree = (list, depth, prefix) => {
151
+ const rows = []
152
+ const cap = Math.min(list.length, RENDER_CAP)
153
+ for (let i = 0; i < cap; i++) {
154
+ const sym = list[i]
155
+ const key = prefix ? prefix + '/' + i : String(i)
156
+ const kids = sym.children && sym.children.length ? sym.children : null
157
+ const isCollapsed = collapsed[key] === true
158
+ const meta = kindMeta(sym.kind)
159
+ const active = cursorLine != null && sym.startLine <= cursorLine && cursorLine <= sym.endLine
160
+ rows.push(React.createElement('div', {
161
+ key,
162
+ className: 'edrv-tree-row' + (active ? ' edrv-tree-active' : ''),
163
+ title: sym.detail || sym.name,
164
+ style: { paddingLeft: 6 + depth * 14 },
165
+ onClick: () => jump(sym),
166
+ },
167
+ React.createElement('span', {
168
+ className: 'edrv-tree-chev',
169
+ onClick: (e) => { e.stopPropagation(); toggleCollapse(key) },
170
+ }, kids ? (isCollapsed ? '▸' : '▾') : ''),
171
+ React.createElement('span', { className: 'edrv-outline-kind ' + meta.cls }, meta.glyph),
172
+ React.createElement('span', { className: 'edrv-tree-name' }, sym.name),
173
+ (sym.detail ? React.createElement('span', { className: 'edrv-outline-detail' }, sym.detail) : null),
174
+ React.createElement('span', { className: 'edrv-outline-ln' }, String(sym.selectLine ?? sym.startLine ?? ''))))
175
+ if (kids && !isCollapsed) rows.push(...renderTree(kids, depth + 1, key))
176
+ }
177
+ if (list.length > RENDER_CAP) {
178
+ rows.push(React.createElement('div', { key: 'cap', className: 'edrv-tree-loading' }, '符号过多,仅显示前 ' + RENDER_CAP + ' 个'))
179
+ }
180
+ return rows
181
+ }
182
+
183
+ const basename = activePath ? String(activePath).split(/[\\/]/).pop() || activePath : ''
184
+
185
+ let body
186
+ if (error) {
187
+ body = React.createElement('div', { className: 'edrv-tree' },
188
+ React.createElement('div', { className: 'edrv-tree-error' },
189
+ React.createElement('span', null, String(error)),
190
+ React.createElement('button', { className: 'edrv-side-btn', onClick: refresh }, '重试')))
191
+ } else if (!activePath) {
192
+ body = React.createElement('div', { className: 'edrv-tree' },
193
+ React.createElement('div', { className: 'edrv-tree-loading' }, '未打开文件'))
194
+ } else if (symbols === null) {
195
+ body = React.createElement('div', { className: 'edrv-tree' },
196
+ React.createElement('div', { className: 'edrv-tree-loading' }, '加载中…'))
197
+ } else if (symbols.length === 0) {
198
+ body = React.createElement('div', { className: 'edrv-tree' },
199
+ React.createElement('div', { className: 'edrv-tree-loading' }, '该语言暂不支持大纲或文件无符号'))
200
+ } else {
201
+ body = React.createElement('div', { className: 'edrv-tree' }, ...renderTree(symbols, 0, ''))
202
+ }
203
+
204
+ return React.createElement('div', { className: 'edrv-side-panel' },
205
+ React.createElement('div', { className: 'edrv-side-head' },
206
+ React.createElement('span', { className: 'edrv-side-title' }, '大纲'),
207
+ React.createElement('span', { className: 'edrv-side-root', title: activePath || '' }, basename),
208
+ React.createElement('span', { style: { flex: 1 } }),
209
+ React.createElement('button', { className: 'edrv-side-btn', title: '折叠全部', disabled: !(symbols && symbols.length), onClick: () => setAll(true) }, '−'),
210
+ React.createElement('button', { className: 'edrv-side-btn', title: '展开全部', disabled: !(symbols && symbols.length), onClick: () => setAll(false) }, '+'),
211
+ React.createElement('button', { className: 'edrv-side-btn', title: '刷新大纲', onClick: refresh }, '⟳')),
212
+ body)
213
+ }
@@ -0,0 +1,24 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — 「大纲」面板定义(注册表一项)。
4
+ * 数据源解析复用 outline/sources 的源注册表(ctx.outlineSources 注入)。
5
+ * 作者 ddj 2026-08-27
6
+ */
7
+ import React from 'react'
8
+ import { OutlinePanel } from './OutlinePanel.js'
9
+ import type { SidebarPanelDef, SidebarCtx } from '../sidebar/types.js'
10
+
11
+ /**
12
+ * 构造大纲面板定义。
13
+ * @author ddj 2026年08月27号
14
+ * @returns 面板定义(无徽标;活动栏图标 📜)
15
+ */
16
+ export function createOutlinePanel(): SidebarPanelDef {
17
+ return {
18
+ id: 'outline',
19
+ title: '大纲',
20
+ icon: '📜',
21
+ order: 20,
22
+ render: (ctx: SidebarCtx) => React.createElement(OutlinePanel, { ctx }),
23
+ }
24
+ }
@@ -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
+ }