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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 中央编辑器(文件页签/QuickOpen/状态栏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚),状态持久化到工作区旁车",
5
5
  "keywords": [
6
6
  "dsh",
@@ -0,0 +1,28 @@
1
+ /**
2
+ * dsh-vscode-mode client — 「在文件浏览器中打开」RPC 包装(树菜单与 Monaco 右键共用)。
3
+ * 归一化为 { ok, error? },不抛异常;无会话/异常降级返回错误文案。
4
+ * 作者 ddj 2026-08-27
5
+ */
6
+ import { rpc } from './rpc.js'
7
+
8
+ export interface RevealOutcome {
9
+ ok: boolean
10
+ error?: string
11
+ }
12
+
13
+ /**
14
+ * 请求 host 在 OS 文件浏览器中打开/定位路径。
15
+ * @author ddj 2026年08月27号
16
+ * @param sessionId 会话 id(可空)
17
+ * @param path 工作区相对路径('' = 根目录)
18
+ * @returns 成功或失败原因
19
+ */
20
+ export async function revealInExplorer(sessionId: string | undefined, path: string): Promise<RevealOutcome> {
21
+ if (!sessionId) return { ok: false, error: '无活动会话' }
22
+ try {
23
+ const res = await rpc('edrv.revealInExplorer', { sessionId, path })
24
+ return res.ok ? { ok: true } : { ok: false, error: res.error }
25
+ } catch (error) {
26
+ return { ok: false, error: '打开异常:' + String(error) }
27
+ }
28
+ }
@@ -28,6 +28,10 @@ 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 { createTreeMenuRegistry } from './sidebar/contextMenu.js'
32
+ import { createDefaultFileMenuItems } from './sidebar/menuItems.js'
33
+ import { createOutlinePanel } from './outline/index.js'
34
+ import { createOutlineSourceRegistry, registerBuiltinOutlineSources } from './outline/sources.js'
31
35
  import type { CompatAdapter } from '../shared/compat.js'
32
36
 
33
37
  export const inject = ['slots', 'timer', 'locale', 'connection', 'remote', 'workspaces', 'sessions', 'conversation', 'settingsScope', 'webUiSettings']
@@ -85,6 +89,17 @@ export function apply(ctx: any): void {
85
89
  const sidebarPanels = createSidebarPanelRegistry()
86
90
  ctx.provide('edrvSidebarPanels', sidebarPanels)
87
91
  ctx.effect(() => sidebarPanels.register(createFilePanel()), 'vscode-mode: sidebar panel')
92
+ // 文件右键菜单项注册表(对外 provide,供本插件/第三方注册;内置「在文件浏览器中打开」)
93
+ const fileMenuItems = createTreeMenuRegistry()
94
+ ctx.provide('edrvFileContextMenuItems', fileMenuItems)
95
+ for (const item of createDefaultFileMenuItems()) {
96
+ ctx.effect(() => fileMenuItems.register(item), 'vscode-mode: file context menu item ' + item.id)
97
+ }
98
+ // 大纲源注册表(公开预留口):第三方语言插件(LSP/VSIX 等)注册更高优先级源即可覆盖兜底
99
+ const outlineSources = createOutlineSourceRegistry()
100
+ ctx.provide('edrvOutlineSources', outlineSources)
101
+ ctx.effect(() => registerBuiltinOutlineSources(outlineSources), 'vscode-mode: outline sources')
102
+ ctx.effect(() => sidebarPanels.register(createOutlinePanel()), 'vscode-mode: sidebar panel outline')
88
103
  ctx.effect(() => registry.register({
89
104
  id: 'system', label: '系统默认应用', priority: 0,
90
105
  open: (path: string) => originalOpenPath.call(workspaces, path),
@@ -126,7 +141,7 @@ export function apply(ctx: any): void {
126
141
  order: 5,
127
142
  label: '文件编辑',
128
143
  inject: (sessionId: string) => ({ sessionId }),
129
- }, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { schedule, addToConversation, sidebarPanels })))
144
+ }, (props: unknown) => React.createElement(EditorView, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems })))
130
145
 
131
146
  // 对话输入框上方差异 dock:普通对话显示单文案按钮,文件编辑页由 EditorView 隐藏
132
147
  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
+ }