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/LICENSE +21 -0
- package/lib/client.js +850 -6
- package/lib/client.js.map +1 -1
- package/lib/index.js +1959 -52
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/events.ts +11 -0
- package/src/client/index.ts +15 -2
- package/src/client/monaco/lsp/index.ts +30 -0
- package/src/client/monaco/lsp/lspClient.ts +192 -0
- package/src/client/monaco/lsp/providers.ts +229 -0
- package/src/client/outline/OutlinePanel.tsx +1 -1
- package/src/client/outline/lspSource.ts +49 -0
- package/src/client/outline/parse.ts +31 -1
- package/src/client/outline/types.ts +2 -0
- package/src/client/styles/editor.css +15 -0
- package/src/client/styles/mcp.css +15 -0
- package/src/client/ui/EditorView.ts +55 -1
- package/src/client/ui/LspSettings.ts +268 -0
- package/src/client/ui/McpSettings.ts +3 -1
- package/src/fileOpenSettings.ts +1 -0
- package/src/index.ts +20 -2
- package/src/lsp/client.ts +205 -0
- package/src/lsp/config.ts +107 -0
- package/src/lsp/extmgr.ts +208 -0
- package/src/lsp/jsonrpc.ts +86 -0
- package/src/lsp/manager.ts +133 -0
- package/src/lsp/providers.ts +235 -0
- package/src/lsp/rpc.ts +334 -0
- package/src/lsp/server.ts +346 -0
- package/src/lsp/settings.ts +44 -0
- package/src/lsp/transport.ts +143 -0
- package/src/lsp/uri.ts +62 -0
- package/src/lsp/zip.ts +82 -0
- package/src/rpc.ts +11 -2
- package/src/shared/lsp.ts +138 -0
- package/src/shared/rpc.ts +33 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.1.
|
|
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",
|
package/src/client/events.ts
CHANGED
|
@@ -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号
|
package/src/client/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
|