dsh-vscode-mode 0.1.32 → 0.1.35

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,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.1.32",
4
- "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚),状态持久化到工作区旁车",
3
+ "version": "0.1.35",
4
+ "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
5
5
  "keywords": [
6
6
  "dsh",
7
7
  "dsh-plugin",
@@ -59,6 +59,17 @@ export function emitOpenEditor(path: string): void {
59
59
  window.dispatchEvent(new CustomEvent('edrv:open-editor', { detail: { path } }))
60
60
  }
61
61
 
62
+ /**
63
+ * 打开指定路径并定位到行列(LSP 跳转目标)。
64
+ * @author ddj 2026年08月27号
65
+ * @param path 目标路径
66
+ * @param line 目标行(1-based,可空)
67
+ * @param column 目标列(1-based,可空)
68
+ */
69
+ export function emitOpenAt(path: string, line?: number, column?: number): void {
70
+ window.dispatchEvent(new CustomEvent('edrv:open-editor', { detail: { path, line, column } }))
71
+ }
72
+
62
73
  /**
63
74
  * 拉起 DiffLauncher。
64
75
  * @author ddj 2026年08月26号
@@ -37,7 +37,9 @@ import { createTreeMenuRegistry } from './sidebar/contextMenu.js'
37
37
  import { createDefaultFileMenuItems } from './sidebar/menuItems.js'
38
38
  import { createOutlinePanel } from './outline/index.js'
39
39
  import { createOutlineSourceRegistry, registerBuiltinOutlineSources } from './outline/sources.js'
40
+ import { createLspOutlineSource } from './outline/lspSource.js'
40
41
  import { keybindingsApply } from './keybindings.js'
42
+ import { setupLsp, setSession } from './monaco/lsp/index.js'
41
43
  import type { CompatAdapter } from '../shared/compat.js'
42
44
 
43
45
  // ⚠️ inject 只列必需服务:webUiSettings 是 @linxin666/dsh-client-ui-web-ui-settings 提供的
@@ -66,17 +68,26 @@ export function apply(ctx: any): void {
66
68
  if (typeof window !== 'undefined' && monacoList && typeof monacoList.subscribe === 'function') {
67
69
  const list = monacoList as { getSnapshot: () => { current?: unknown }; subscribe: (listener: () => void) => () => void }
68
70
  const schedulePreload = () => {
69
- const preload = () => loadMonaco(() => {}).catch(() => {})
71
+ const preload = () => loadMonaco(() => {}).then((m: unknown) => setupLsp(m)).catch(() => {})
70
72
  if (typeof window.requestIdleCallback === 'function') window.requestIdleCallback(preload, { timeout: 2000 })
71
73
  else schedule(preload, 300)
72
74
  }
73
75
  const onSessionEnter = () => {
74
- if (!list.getSnapshot()?.current) return
76
+ const current = list.getSnapshot()?.current
77
+ if (!current) return
78
+ setSession(current)
75
79
  schedulePreload()
76
80
  }
77
81
  ctx.effect(() => list.subscribe(onSessionEnter), 'vscode-mode: monaco session trigger')
78
82
  onSessionEnter()
79
83
  }
84
+ // 会话切换 → LSP 文档归属更新 + 状态刷新
85
+ if (monacoList && typeof monacoList.subscribe === 'function') {
86
+ const list = monacoList as { getSnapshot: () => { current?: string }; subscribe: (listener: () => void) => () => void }
87
+ ctx.effect(() => list.subscribe(() => {
88
+ setSession(list.getSnapshot()?.current)
89
+ }), 'vscode-mode: lsp session sync')
90
+ }
80
91
  const originalOpenPath = workspaces?.openPath
81
92
  const binder = pickSettingsBinder(ctx)
82
93
  const settings = binder.scope
@@ -113,6 +124,8 @@ export function apply(ctx: any): void {
113
124
  const outlineSources = createOutlineSourceRegistry()
114
125
  ctx.provide('edrvOutlineSources', outlineSources)
115
126
  ctx.effect(() => registerBuiltinOutlineSources(outlineSources), 'vscode-mode: outline sources')
127
+ // LSP 大纲源(priority 60):lua/csharp 有 host 服务器时优先生效,否则自动落入 fallback
128
+ ctx.effect(() => outlineSources.register(createLspOutlineSource()), 'vscode-mode: outline lsp source')
116
129
  ctx.effect(() => sidebarPanels.register(createOutlinePanel()), 'vscode-mode: sidebar panel outline')
117
130
  ctx.effect(() => registry.register({
118
131
  id: 'system', label: '系统默认应用', priority: 0,
@@ -0,0 +1,30 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — LSP 装配编排。
4
+ * setupLsp(monaco):注册 Monaco provider 与文档跟踪(幂等);调用方在 Monaco 加载后调用。
5
+ * setSession(id):会话切换时同步(provider 与文档同步共用)。
6
+ * 作者 ddj 2026-08-27
7
+ */
8
+ import { registerLspProviders, hideReferencesOverlay } from './providers.js'
9
+ import { setLspSession, refreshStatus, lspStatusFor, onLspProgress } from './lspClient.js'
10
+
11
+ let monacoRef = null
12
+
13
+ /** Monaco 加载后装配(幂等;重复调用仅刷新会话)。 */
14
+ export function setupLsp(monaco) {
15
+ monacoRef = monaco
16
+ registerLspProviders(monaco)
17
+ }
18
+
19
+ /** 会话切换:更新 host 侧文档归属 + 刷新状态。 */
20
+ export function setSession(sessionId) {
21
+ setLspSession(sessionId)
22
+ void refreshStatus(true)
23
+ }
24
+
25
+ /** 编辑器卸载时清理浮动面板。 */
26
+ export function disposeLspOverlay() {
27
+ hideReferencesOverlay()
28
+ }
29
+
30
+ export { lspStatusFor, refreshStatus, onLspProgress }
@@ -0,0 +1,192 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — LSP 会话与文档同步(模块级单例)。
4
+ * 跟踪当前会话 + Monaco 打开文档(model 事件驱动),去抖推送 edrv.lsp.sync;
5
+ * 并提供 definition/references/documentSymbol/hover/status 的 RPC 薄封装。
6
+ * 作者 ddj 2026-08-27
7
+ */
8
+ import { rpc } from '../../rpc.js'
9
+ import { monoToLsp } from '../../../shared/lsp.js'
10
+
11
+ /** edrv:// 模型 URI → 工作区相对/绝对路径(与 host 侧 path 对齐)。 */
12
+ export function pathOfModel(model) {
13
+ if (!model || !model.uri) return null
14
+ const uri = model.uri
15
+ if (uri.scheme !== 'edrv') return null
16
+ const raw = uri.path ?? ''
17
+ try {
18
+ const decoded = decodeURIComponent(String(raw).replace(/^\//, ''))
19
+ return decoded || null
20
+ } catch (error) {
21
+ return String(raw).replace(/^\//, '') || null
22
+ }
23
+ }
24
+
25
+ const SYNC_DEBOUNCE_MS = 250
26
+
27
+ let sessionId
28
+ const syncTimers = new Map()
29
+ let statusCache = { servers: [], at: 0 }
30
+ let progressListeners = new Set()
31
+ let ready = false
32
+
33
+ /** 设置当前会话(切换会话时清理旧文档跟踪由 host 端 session/disposed 兜底)。 */
34
+ export function setLspSession(id) {
35
+ if (sessionId === id) return
36
+ sessionId = id
37
+ void refreshStatus()
38
+ }
39
+
40
+ export function lspSessionId() {
41
+ return sessionId
42
+ }
43
+
44
+ /** 订阅 LSP 状态变化(服务器进度/启动状态)。 */
45
+ export function onLspProgress(listener) {
46
+ if (typeof listener !== 'function') return () => {}
47
+ progressListeners.add(listener)
48
+ return () => progressListeners.delete(listener)
49
+ }
50
+
51
+ /** 发布当前 server 状态给编辑器底部状态区。 */
52
+ function publishProgress() {
53
+ for (const listener of progressListeners) {
54
+ try { listener(statusCache.servers) } catch (error) { /* UI listener failure ignored */ }
55
+ }
56
+ }
57
+
58
+ /** 立即同步某文档到 host(首次打开/查询前保证服务器已认识该文档)。 */
59
+ export async function syncDoc(path, text, immediate = false) {
60
+ if (!path || typeof text !== 'string') return
61
+ if (immediate) {
62
+ clearPending(path)
63
+ await rpc('edrv.lsp.sync', { sessionId, path, text, version: Date.now() }).catch(() => {})
64
+ return
65
+ }
66
+ clearPending(path)
67
+ syncTimers.set(path, setTimeout(() => {
68
+ syncTimers.delete(path)
69
+ rpc('edrv.lsp.sync', { sessionId, path, text, version: Date.now() }).catch(() => {})
70
+ }, SYNC_DEBOUNCE_MS))
71
+ }
72
+
73
+ function clearPending(path) {
74
+ const timer = syncTimers.get(path)
75
+ if (timer) { clearTimeout(timer); syncTimers.delete(path) }
76
+ }
77
+
78
+ /** 文档关闭:停止去抖并通知 host。 */
79
+ export function closeDoc(path) {
80
+ clearPending(path)
81
+ void rpc('edrv.lsp.close', { sessionId, path }).catch(() => {})
82
+ }
83
+
84
+ /** 立即推送 + 查询定义;返回 [{ uri, range }](uri 已转 edrv://)。 */
85
+ export async function findDefinition(path, text, position) {
86
+ await syncDoc(path, text, true)
87
+ const res = await rpc('edrv.lsp.definition', { sessionId, path, position: monoToLsp(position.lineNumber, position.column) }).catch(() => null)
88
+ if (!res || !res.ok || !Array.isArray(res.locations)) return []
89
+ return res.locations.map(toClientLocation)
90
+ }
91
+
92
+ /** 立即推送 + 查询引用。 */
93
+ export async function findReferences(path, text, position, includeDeclaration = false) {
94
+ await syncDoc(path, text, true)
95
+ const res = await rpc('edrv.lsp.references', { sessionId, path, position: monoToLsp(position.lineNumber, position.column), includeDeclaration }).catch(() => null)
96
+ if (!res || !res.ok || !Array.isArray(res.locations)) return []
97
+ return res.locations.map(toClientLocation)
98
+ }
99
+
100
+ /** 查询大纲符号(documentSymbol)。 */
101
+ export async function fetchDocumentSymbols(path, text) {
102
+ await syncDoc(path, text, true)
103
+ const res = await rpc('edrv.lsp.documentSymbol', { sessionId, path }).catch(() => null)
104
+ if (!res || !res.ok || !Array.isArray(res.symbols)) return []
105
+ return res.symbols
106
+ }
107
+
108
+ /** 查询 hover 内容。 */
109
+ export async function fetchHover(path, text, position) {
110
+ await syncDoc(path, text, true)
111
+ const res = await rpc('edrv.lsp.hover', { sessionId, path, position: monoToLsp(position.lineNumber, position.column) }).catch(() => null)
112
+ if (!res || !res.ok) return null
113
+ return res.hover || null
114
+ }
115
+
116
+ /** 拉取服务器状态(带 5s 缓存)。 */
117
+ export async function refreshStatus(force = false) {
118
+ if (!force && Date.now() - statusCache.at < 1500) return statusCache.servers
119
+ const res = await rpc('edrv.lsp.status', { sessionId }).catch(() => null)
120
+ const servers = res && res.ok && Array.isArray(res.servers) ? res.servers : []
121
+ statusCache = { servers, at: Date.now() }
122
+ ready = true
123
+ publishProgress()
124
+ return servers
125
+ }
126
+
127
+ export function lspReady() {
128
+ return ready
129
+ }
130
+
131
+ /** 当前会话某语言服务器状态(就绪/索引中/不可用)。 */
132
+ export function lspStatusFor(languageId) {
133
+ const servers = statusCache.servers
134
+ const match = servers.find((s) => s.languageId === languageId && s.root)
135
+ return match || servers.find((s) => s.languageId === languageId)
136
+ }
137
+
138
+ /** file:// 或已转 edrv:// 的 LSP location → Monaco Location(edrv:// uri + 1-based range)。 */
139
+ function toClientLocation(loc) {
140
+ const range = loc.range || {}
141
+ const start = range.start || {}
142
+ const end = range.end || {}
143
+ return {
144
+ uri: window.monaco.Uri.parse(toEdrvUri(loc.uri, loc.root)),
145
+ range: new window.monaco.Range(
146
+ Math.max(1, (start.line ?? 0) + 1),
147
+ Math.max(1, (start.character ?? 0) + 1),
148
+ Math.max(1, (end.line ?? 0) + 1),
149
+ Math.max(1, (end.character ?? 0) + 1),
150
+ ),
151
+ }
152
+ }
153
+
154
+ /** file:// URI(或裸路径)→ edrv:// 相对路径(去掉盘符/根,与编辑器模型路径一致)。 */
155
+ export function toEdrvUri(uri, root) {
156
+ if (!uri) return ''
157
+ if (uri.startsWith('edrv://')) return uri
158
+ const path = relativeLspPath(uri, root)
159
+ return 'edrv:///' + encodeURI(path)
160
+ }
161
+
162
+ /** 将 LSP 目标解析为当前工作区相对路径(跨文件跳转与 model URI 对齐)。 */
163
+ export function targetOpenPath(loc) {
164
+ return relativeLspPath(loc?.uri, loc?.root)
165
+ }
166
+
167
+ /** file URI/裸路径 → root 下的 / 分隔相对路径;root 不匹配时保留规范化绝对路径。 */
168
+ function relativeLspPath(uri, root) {
169
+ if (!uri) return ''
170
+ let path = String(uri)
171
+ if (path.startsWith('file://')) {
172
+ try { path = decodeURIComponent(path.replace(/^file:\/\//, '')) } catch (error) { path = path.replace(/^file:\/\//, '') }
173
+ }
174
+ path = path.replace(/\\/g, '/').replace(/^\/([A-Za-z]:)/, '$1')
175
+ const normalizedRoot = root ? String(root).replace(/\\/g, '/').replace(/^\/([A-Za-z]:)/, '$1').replace(/\/+$/, '') : ''
176
+ const lowerPath = path.toLowerCase()
177
+ const lowerRoot = normalizedRoot.toLowerCase()
178
+ if (normalizedRoot && (lowerPath === lowerRoot || lowerPath.startsWith(lowerRoot + '/'))) {
179
+ return path.slice(normalizedRoot.length).replace(/^\/+/, '')
180
+ }
181
+ return path.replace(/^\/+/, '')
182
+ }
183
+
184
+ /** 目标行/列(0-based LSP → 1-based Monaco)。 */
185
+ export function targetMonoPosition(loc) {
186
+ const range = loc.range || {}
187
+ const start = range.start || {}
188
+ return {
189
+ line: Math.max(1, (start.line ?? 0) + 1),
190
+ column: Math.max(1, (start.character ?? 0) + 1),
191
+ }
192
+ }
@@ -0,0 +1,229 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — Monaco LSP provider 注册 + 跳转/引用命令。
4
+ * 数据层:Definition/Reference/DocumentSymbol/Hover provider(同一文件内跳转 Monaco 原生可用)。
5
+ * 交互层:edrv.goToDefinition(F12)/ edrv.findReferences(Shift+F12 / 右键),跨文件跳转自研
6
+ * (target 经 openFileAt 打开并定位),引用结果用轻量浮动面板展示(点击跳转)。
7
+ * 作者 ddj 2026-08-27
8
+ */
9
+ import {
10
+ pathOfModel, syncDoc, closeDoc, findDefinition, findReferences,
11
+ fetchDocumentSymbols, fetchHover, targetOpenPath, targetMonoPosition, lspStatusFor,
12
+ } from './lspClient.js'
13
+
14
+ const LSP_LANGS = ['lua', 'csharp']
15
+ let registered = false
16
+ const disposables = []
17
+ let overlayEl = null
18
+
19
+ /** 目标文件打开并定位(复用现有 openFileAt 的 edrv:open-editor 事件通道)。 */
20
+ function openAt(path, line, column) {
21
+ window.dispatchEvent(new CustomEvent('edrv:open-editor', { detail: { path, line, column } }))
22
+ }
23
+
24
+ function jumpToLocation(loc) {
25
+ const path = targetOpenPath(loc)
26
+ const pos = targetMonoPosition(loc)
27
+ openAt(path, pos.line, pos.column)
28
+ }
29
+
30
+ /** 注册全部 Monaco LSP provider 与文档跟踪(幂等)。 */
31
+ export function registerLspProviders(monaco) {
32
+ if (registered) return
33
+ registered = true
34
+
35
+ // —— 文档跟踪:model 生命周期 → host 文档同步(含未保存编辑)——
36
+ const attachModel = (model) => {
37
+ if (!model || model.uri.scheme !== 'edrv') return
38
+ const path = pathOfModel(model)
39
+ if (!path) return
40
+ const contentDisposer = model.onDidChangeContent(() => syncDoc(path, model.getValue()))
41
+ const disposeDisposer = model.onWillDispose(() => {
42
+ closeDoc(path)
43
+ contentDisposer.dispose()
44
+ })
45
+ disposables.push(contentDisposer, disposeDisposer)
46
+ syncDoc(path, model.getValue(), true)
47
+ }
48
+ disposables.push(monaco.editor.onDidCreateModel(attachModel))
49
+ for (const model of monaco.editor.getModels()) attachModel(model)
50
+
51
+ // —— 数据 provider ——
52
+ disposables.push(
53
+ monaco.languages.registerDefinitionProvider(LSP_LANGS, {
54
+ provideDefinition: (model, position, token) => {
55
+ const path = pathOfModel(model)
56
+ if (!path) return []
57
+ return findDefinition(path, model.getValue(), position)
58
+ },
59
+ }),
60
+ monaco.languages.registerReferenceProvider(LSP_LANGS, {
61
+ provideReferences: (model, position, context, token) => {
62
+ const path = pathOfModel(model)
63
+ if (!path) return []
64
+ return findReferences(path, model.getValue(), position, Boolean(context?.includeDeclaration))
65
+ },
66
+ }),
67
+ monaco.languages.registerDocumentSymbolProvider(LSP_LANGS, {
68
+ provideDocumentSymbols: async (model, token) => {
69
+ const path = pathOfModel(model)
70
+ if (!path) return []
71
+ const symbols = await fetchDocumentSymbols(path, model.getValue())
72
+ return symbols.map(toMonacoSymbol)
73
+ },
74
+ }),
75
+ monaco.languages.registerHoverProvider(LSP_LANGS, {
76
+ provideHover: async (model, position, token) => {
77
+ const path = pathOfModel(model)
78
+ if (!path) return null
79
+ const hover = await fetchHover(path, model.getValue(), position)
80
+ if (!hover || !hover.contents || !hover.contents.length) return null
81
+ const range = hover.range
82
+ ? new monaco.Range(hover.range.start.line + 1, hover.range.start.character + 1, hover.range.end.line + 1, hover.range.end.character + 1)
83
+ : null
84
+ return {
85
+ contents: hover.contents.map((text) => ({ value: text })),
86
+ range: range || undefined,
87
+ }
88
+ },
89
+ }),
90
+ )
91
+ }
92
+
93
+ /** LSP SymbolInfo(host 归一化后)→ Monaco DocumentSymbol。 */
94
+ function toMonacoSymbol(symbol) {
95
+ const range = symbol.range || {}
96
+ const sel = symbol.selectionRange || range
97
+ const s = (p) => ({ lineNumber: Math.max(1, (p.line ?? 0) + 1), column: Math.max(1, (p.character ?? 0) + 1) })
98
+ const out = {
99
+ name: symbol.name,
100
+ detail: symbol.detail || '',
101
+ kind: symbol.kind ?? 0,
102
+ tags: [],
103
+ range: new window.monaco.Range(s(range.start).lineNumber, s(range.start).column, s(range.end).lineNumber, s(range.end).column),
104
+ selectionRange: new window.monaco.Range(s(sel.start).lineNumber, s(sel.start).column, s(sel.end).lineNumber, s(sel.end).column),
105
+ }
106
+ if (Array.isArray(symbol.children) && symbol.children.length) {
107
+ out.children = symbol.children.map(toMonacoSymbol)
108
+ }
109
+ return out
110
+ }
111
+
112
+ /** 定义查询并跳转;nativeReveal 仅供 F12 保留 Monaco 原生 reveal。 */
113
+ async function goToDefinition(ed, position, nativeReveal = false) {
114
+ const model = ed.getModel()
115
+ const path = pathOfModel(model)
116
+ if (!path || !position) return
117
+ const locations = await findDefinition(path, model.getValue(), position)
118
+ if (!locations.length) {
119
+ setStatus('未找到定义')
120
+ return
121
+ }
122
+ const first = locations[0]
123
+ if (nativeReveal && targetOpenPath(first) === path) {
124
+ const action = ed.getAction('editor.action.revealDefinition')
125
+ if (action) {
126
+ await action.run()
127
+ return
128
+ }
129
+ }
130
+ jumpToLocation(first)
131
+ }
132
+
133
+ /** F12 / 右键「转到定义」:查询定义并跳转(首个目标)。 */
134
+ export async function runGoToDefinition(ed) {
135
+ await goToDefinition(ed, ed.getPosition(), true)
136
+ }
137
+
138
+ /** Ctrl+鼠标点击定义:使用点击位置查询,避免依赖当前光标位置。 */
139
+ export async function gotoDefinitionAt(ed, position) {
140
+ await goToDefinition(ed, position)
141
+ }
142
+
143
+ /** 给一个 Monaco 编辑器绑定 Ctrl/Cmd+鼠标定义跳转(幂等)。 */
144
+ export function bindLspEditor(ed) {
145
+ if (!ed || ed.__edrvLspClick) return
146
+ ed.__edrvLspClick = true
147
+ const disposable = ed.onMouseDown((event) => {
148
+ const native = event?.event
149
+ const position = event?.target?.position
150
+ const button = native?.button ?? event?.button
151
+ const ctrl = native?.ctrlKey ?? event?.ctrlKey
152
+ const meta = native?.metaKey ?? event?.metaKey
153
+ const isLeftButton = button === 0 || native?.leftButton === true
154
+ if (!native || !position || !isLeftButton || (!ctrl && !meta)) return
155
+ native.preventDefault?.()
156
+ native.stopPropagation?.()
157
+ void gotoDefinitionAt(ed, position)
158
+ })
159
+ disposables.push(disposable)
160
+ }
161
+
162
+ /** Shift+F12 / 右键「查找所有引用」:查询引用并展示浮动面板。 */
163
+ export async function runFindReferences(ed) {
164
+ const model = ed.getModel()
165
+ const path = pathOfModel(model)
166
+ const position = ed.getPosition()
167
+ if (!path || !position) return
168
+ const locations = await findReferences(path, model.getValue(), position, false)
169
+ showReferencesOverlay(ed, path, position, locations)
170
+ }
171
+
172
+ /** 引用结果浮动面板(轻量 DOM,点击跳转;Esc/失焦关闭)。 */
173
+ function showReferencesOverlay(ed, path, position, locations) {
174
+ hideReferencesOverlay()
175
+ if (!locations.length) {
176
+ setStatus('未找到引用')
177
+ return
178
+ }
179
+ const root = ed.getDomNode()
180
+ if (!root) return
181
+ const el = document.createElement('div')
182
+ el.className = 'edrv-refs'
183
+ const header = document.createElement('div')
184
+ header.className = 'edrv-refs-head'
185
+ header.textContent = '引用 ' + locations.length + ' 处'
186
+ el.appendChild(header)
187
+ const list = document.createElement('div')
188
+ list.className = 'edrv-refs-list'
189
+ for (const loc of locations) {
190
+ const row = document.createElement('div')
191
+ row.className = 'edrv-refs-row'
192
+ const pos = targetMonoPosition(loc)
193
+ const rel = targetOpenPath(loc)
194
+ const name = String(rel).split('/').pop() || rel
195
+ row.textContent = name + ':' + pos.line + ':' + pos.column
196
+ row.title = rel + ':' + pos.line
197
+ row.addEventListener('click', () => {
198
+ hideReferencesOverlay()
199
+ jumpToLocation(loc)
200
+ })
201
+ list.appendChild(row)
202
+ }
203
+ el.appendChild(list)
204
+ root.appendChild(el)
205
+ overlayEl = el
206
+ const onKey = (e) => { if (e.key === 'Escape') hideReferencesOverlay() }
207
+ window.addEventListener('keydown', onKey)
208
+ el.addEventListener('click', (e) => { if (e.target === el) hideReferencesOverlay() })
209
+ el._onKey = onKey
210
+ }
211
+
212
+ export function hideReferencesOverlay() {
213
+ if (overlayEl) {
214
+ window.removeEventListener('keydown', overlayEl._onKey)
215
+ overlayEl.remove()
216
+ overlayEl = null
217
+ }
218
+ }
219
+
220
+ /** 编辑器状态栏提示(复用 EditorView 的 status 通道)。 */
221
+ function setStatus(text) {
222
+ window.dispatchEvent(new CustomEvent('edrv:status', { detail: { text } }))
223
+ }
224
+
225
+ /** 当前会话与语言就绪状态(供编辑器状态点)。 */
226
+ export function lspStatusDot(languageId) {
227
+ const status = lspStatusFor(languageId)
228
+ return status ? status.phase : 'none'
229
+ }
@@ -65,7 +65,7 @@ export function OutlinePanel(props) {
65
65
  const sources = sourcesRef.current
66
66
  if (!sources) { setSymbols([]); setError(null); return }
67
67
  setError(null)
68
- resolveOutline(sources, { languageId: model.getLanguageId(), model, editor: ed, monaco: window.monaco })
68
+ resolveOutline(sources, { languageId: model.getLanguageId(), path: modelPath, model, editor: ed, monaco: window.monaco })
69
69
  .then((list) => { if (seq === seqRef.current) setSymbols(list || []) })
70
70
  .catch((e) => { if (seq === seqRef.current) { setError(String(e?.message ?? e)); setSymbols(null) } })
71
71
  }, [])
@@ -0,0 +1,49 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — 大纲「lsp」源:经 host LSP documentSymbol 解析(优先于 monaco/fallback)。
4
+ * LSP 服务器未就绪时返回 [] → resolveOutline 自动落入下一优先级(fallback 兜底),不阻塞面板。
5
+ * 作者 ddj 2026-08-27
6
+ */
7
+ import type { OutlineSource, OutlineSourceInput, OutlineSymbol } from './types.js'
8
+ import { fetchDocumentSymbols } from '../monaco/lsp/lspClient.js'
9
+
10
+ export const LSP_OUTLINE_PRIORITY = 60
11
+
12
+ /** LSP 大纲源:lua/csharp 等有 host 服务器能力的语言。 */
13
+ export function createLspOutlineSource(): OutlineSource {
14
+ return {
15
+ id: 'lsp',
16
+ priority: LSP_OUTLINE_PRIORITY,
17
+ provides: (languageId: string) => languageId === 'lua' || languageId === 'csharp',
18
+ async get(input: OutlineSourceInput): Promise<OutlineSymbol[]> {
19
+ const model = input.model
20
+ if (!model || typeof model.getValue !== 'function') return []
21
+ const path = input.path || (model.uri && model.uri.path ? decodeURIComponent(String(model.uri.path).replace(/^\//, '')) : null)
22
+ if (!path) return []
23
+ const symbols = await fetchDocumentSymbols(path, model.getValue())
24
+ return (Array.isArray(symbols) ? symbols : []).map(toOutline).filter(Boolean)
25
+ },
26
+ }
27
+ }
28
+
29
+ /** LSP SymbolInfo(host 归一化,0-based range)→ 大纲 OutlineSymbol(1-based)。 */
30
+ function toOutline(symbol) {
31
+ if (!symbol || typeof symbol.name !== 'string') return null
32
+ const range = symbol.range || {}
33
+ const sel = symbol.selectionRange || range
34
+ const start = range.start || {}
35
+ const select = sel.start || {}
36
+ const out = {
37
+ name: symbol.name,
38
+ kind: typeof symbol.kind === 'number' ? symbol.kind : 0,
39
+ startLine: Math.max(1, (start.line ?? 0) + 1),
40
+ endLine: Math.max(1, ((range.end?.line ?? start.line) ?? 0) + 1),
41
+ selectLine: Math.max(1, (select.line ?? start.line ?? 0) + 1),
42
+ }
43
+ if (typeof symbol.detail === 'string' && symbol.detail) out.detail = symbol.detail
44
+ if (Array.isArray(symbol.children) && symbol.children.length) {
45
+ const kids = symbol.children.map(toOutline).filter(Boolean)
46
+ if (kids.length) out.children = kids
47
+ }
48
+ return out
49
+ }
@@ -129,12 +129,42 @@ function parsePwsh(text: string): OutlineSymbol[] {
129
129
  return out
130
130
  }
131
131
 
132
- /** Lua:`function Foo:bar` / `M.bar = class|function`。 */
132
+ /** Lua:`function Foo:bar` / `M.bar = class|function` + EmmyLua 注解(---@class/@alias/@enum/@field)。 */
133
133
  function parseLua(text: string): OutlineSymbol[] {
134
134
  const out: OutlineSymbol[] = []
135
135
  const lines = text.split('\n')
136
+ /** 当前注解类(收拢紧随的 ---@field 为字段子节点)。 */
137
+ let pendingClass: OutlineSymbol | null = null
136
138
  for (let i = 0; i < lines.length; i++) {
137
139
  const line = lines[i]
140
+ // EmmyLua 注解(---@):class/alias/enum 进大纲根,field 收给当前类
141
+ const am = /^[ \t]*---@(class|alias|enum|field|type)\s+([^\r\n]+)/.exec(line)
142
+ if (am) {
143
+ const tag = am[1]
144
+ const rest = am[2].trim()
145
+ const name = (rest.match(/^([A-Za-z_][\w]*)/) || [])[1] ?? ''
146
+ if (tag === 'class') {
147
+ if (name) {
148
+ const item = mk(i, name, SK.Class, line)
149
+ item.children = []
150
+ out.push(item)
151
+ pendingClass = item
152
+ } else pendingClass = null
153
+ } else if (tag === 'alias') {
154
+ if (name) out.push(mk(i, name, SK.TypeParameter, line))
155
+ pendingClass = null
156
+ } else if (tag === 'enum') {
157
+ if (name) out.push(mk(i, name, SK.Enum, line))
158
+ pendingClass = null
159
+ } else if (tag === 'field') {
160
+ if (name && pendingClass) {
161
+ pendingClass.children!.push(mk(i, name, SK.Field, line))
162
+ }
163
+ } else if (tag === 'type') {
164
+ pendingClass = null
165
+ }
166
+ continue
167
+ }
138
168
  if (/^\s*--/.test(line)) continue
139
169
  const fm = /^[ \t]*(?:local[ \t]+)?function[ \t]+([A-Za-z_][\w.:]*)/.exec(line)
140
170
  if (fm) { out.push(mk(i, fm[1], SK.Function, line)); continue }
@@ -24,6 +24,8 @@ export interface OutlineSymbol {
24
24
  /** 大纲数据源输入(面板解析时注入当前快照;字段均为弱类型,源自行 duck-typing)。 */
25
25
  export interface OutlineSourceInput {
26
26
  languageId: string
27
+ /** 活动文件路径(工作区相对,可空;LSP 等源用)。 */
28
+ path?: string
27
29
  /** Monaco 文本模型(源可用 model.getValue() 自行解析)。 */
28
30
  model?: unknown
29
31
  /** 当前 Monaco 编辑器实例(供 _commandService 等内部调用)。 */
@@ -33,6 +33,13 @@
33
33
  [data-edrv-view] .edrv-btn-mini:disabled { opacity: .45; cursor: default; }
34
34
  [data-edrv-view] .edrv-statusbar { display: flex; align-items: center; gap: 10px; padding: 3px 10px; border-top: 1px solid var(--dsw-alias-border-l1, #333); background: var(--dsw-alias-bg-layer-1, transparent); font-size: 11px; color: var(--dsw-alias-label-tertiary, #888); flex-shrink: 0; white-space: nowrap; overflow: hidden; }
35
35
  [data-edrv-view] .edrv-statusbar .edrv-sp-path { flex: 1; overflow: hidden; text-overflow: ellipsis; }
36
+ [data-edrv-view] .edrv-statusbar .edrv-sp-lsp { flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; }
37
+ [data-edrv-view] .edrv-statusbar .edrv-sp-progress-message { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--dsw-alias-label-secondary, #52606d); }
38
+ [data-edrv-view] .edrv-statusbar .edrv-sp-progress { flex: 0 0 auto; min-width: 36px; text-align: right; font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-secondary, #52606d); }
39
+ [data-edrv-view] .edrv-lsp-status-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; background: var(--dsw-alias-label-tertiary, #9aa5b1); }
40
+ [data-edrv-view] .edrv-lsp-status-dot.ready { background: var(--dsw-alias-state-success-primary, #0f9d58); }
41
+ [data-edrv-view] .edrv-lsp-status-dot.busy { background: var(--dsw-alias-state-warn-primary, #d97706); animation: edrv-lsp-pulse 1.2s ease-in-out infinite; }
42
+ @keyframes edrv-lsp-pulse { 50% { opacity: .45; } }
36
43
  [data-edrv-view] .edrv-diffchip { font-size: 11px; padding: 2px 10px; border: 1px solid var(--dsw-alias-border-l1, #333); border-radius: 999px; background: var(--dsw-alias-bg-layer-2, transparent); color: var(--dsw-alias-state-warn-primary, #b7791f); cursor: pointer; white-space: nowrap; flex-shrink: 0; }
37
44
  [data-edrv-view] .edrv-chip-btn { font-size: 12px; padding: 2px 6px; border: none; background: transparent; color: var(--dsw-alias-label-secondary, #aaa); cursor: pointer; border-radius: 4px; flex-shrink: 0; }
38
45
  [data-edrv-view] .edrv-chip-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.06)); }
@@ -284,3 +291,11 @@
284
291
  [data-edrv-view] .edrv-search-toggle.on { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.14)); color: var(--dsw-alias-label-primary, #1f2933); border-color: var(--dsw-alias-brand-primary, #0f9d58); }
285
292
  [data-edrv-view] .edrv-search-toggle:disabled { opacity: .4; cursor: default; }
286
293
 
294
+ /* LSP 引用结果浮动面板(Shift+F12 / 右键「查找所有引用」) */
295
+ [data-edrv-view] .edrv-refs { position: absolute; right: 12px; top: 40px; z-index: 60; width: 360px; max-height: 60%; display: flex; flex-direction: column; border: 1px solid var(--dsw-alias-border-l2, #555); border-radius: 8px; background: var(--dsw-alias-bg-panel, #161b22); box-shadow: 0 6px 24px rgba(0,0,0,.4); overflow: hidden; font-size: 12px; }
296
+ [data-edrv-view] .edrv-refs-head { padding: 6px 10px; font-weight: 600; color: var(--dsw-alias-label-primary, #eee); border-bottom: 1px solid var(--dsw-alias-border-l2, #333); background: var(--dsw-alias-bg-layer-1, transparent); }
297
+ [data-edrv-view] .edrv-refs-list { overflow-y: auto; }
298
+ [data-edrv-view] .edrv-refs-row { padding: 4px 10px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--dsw-alias-label-secondary, #c0c8d0); font-family: var(--ds-font-family-code, ui-monospace, monospace); }
299
+ [data-edrv-view] .edrv-refs-row:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.10)); color: var(--dsw-alias-label-primary, #fff); }
300
+
301
+