dsh-vscode-mode 0.1.45 → 0.1.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/lib/client.js +168 -2
- package/lib/client.js.map +1 -1
- package/lib/index.js +105 -5
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/monaco/lsp/providers.ts +10 -1
- package/src/client/monaco/lsp/underline.ts +101 -0
- package/src/client/styles/editor.css +2 -0
- package/src/client/ui/EditorView.ts +2 -0
- package/src/lsp/client.ts +4 -0
- package/src/lsp/derive.ts +96 -0
- package/src/lsp/server.ts +50 -4
- package/src/shared/lsp.ts +57 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -197,7 +197,16 @@ export async function gotoRefsAt(ed, position) {
|
|
|
197
197
|
if (!path || !position) return
|
|
198
198
|
const all = await findReferences(path, model.getValue(), position, false)
|
|
199
199
|
const others = filterOtherRefs(all, path, position)
|
|
200
|
-
if (!others.length)
|
|
200
|
+
if (!others.length) {
|
|
201
|
+
// 0 条「其它」引用:降级查定义(参数/局部变量只被声明时引用为空,定义可经 host 降级链推导)
|
|
202
|
+
const defs = await findDefinition(path, model.getValue(), position)
|
|
203
|
+
const first = defs[0]
|
|
204
|
+
if (!first) return
|
|
205
|
+
const lspPos = monoToLsp(position.lineNumber, position.column)
|
|
206
|
+
if (sameFile(targetOpenPath(first), path) && posInRange(lspPos, first.lspRange)) return // 定义即自身:无其它可跳,保持静默
|
|
207
|
+
jumpToLocation(first)
|
|
208
|
+
return
|
|
209
|
+
}
|
|
201
210
|
if (others.length === 1) {
|
|
202
211
|
jumpToLocation(others[0])
|
|
203
212
|
return
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — Ctrl+hover 可导航标识符下划线提示。
|
|
4
|
+
* 背景:内嵌 Monaco 构建(0.42.0-dev)无 linkDetection contrib,原生不产生 ctrl+hover 下划线,
|
|
5
|
+
* 插件自绘:Ctrl/Meta 按住悬停在"语义 token 标识符"上时给词加下划线装饰,移出/松开即清除。
|
|
6
|
+
* 数据来源于语义 token(每 model+version 缓存 60s),不做逐词 LSP 查询。
|
|
7
|
+
* 作者 ddj 2026-09-02
|
|
8
|
+
*/
|
|
9
|
+
import { pathOfModel, fetchSemanticTokens } from './lspClient.js'
|
|
10
|
+
import { decodeSemanticTokens, isNavigableTokenType } from '../../../shared/lsp.js'
|
|
11
|
+
|
|
12
|
+
const CACHE_TTL_MS = 60_000
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 给一个 Monaco 编辑器绑定 Ctrl+hover 下划线提示(幂等)。
|
|
16
|
+
* @author ddj 2026年09月02号
|
|
17
|
+
* @param ed Monaco 编辑器
|
|
18
|
+
* @param monaco Monaco 实例
|
|
19
|
+
*/
|
|
20
|
+
export function bindLspUnderline(ed, monaco) {
|
|
21
|
+
if (!ed || !monaco || ed.__edrvUnderline) return
|
|
22
|
+
ed.__edrvUnderline = true
|
|
23
|
+
let decoIds = []
|
|
24
|
+
let lastKey = null
|
|
25
|
+
let hoverSeq = 0
|
|
26
|
+
let cache = { path: null, version: -1, ranges: null, at: 0 }
|
|
27
|
+
|
|
28
|
+
const clear = () => {
|
|
29
|
+
if (decoIds.length) {
|
|
30
|
+
ed.deltaDecorations(decoIds, [])
|
|
31
|
+
decoIds = []
|
|
32
|
+
}
|
|
33
|
+
lastKey = null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 取当前文档语义 token 范围(模型版本 + TTL 缓存)。 */
|
|
37
|
+
const rangesOf = async (model) => {
|
|
38
|
+
const path = pathOfModel(model)
|
|
39
|
+
const version = model.getVersionId?.() ?? 0
|
|
40
|
+
if (cache.path === path && cache.version === version && cache.ranges && Date.now() - cache.at < CACHE_TTL_MS) {
|
|
41
|
+
return cache.ranges
|
|
42
|
+
}
|
|
43
|
+
const res = await fetchSemanticTokens(path, model.getValue()).catch(() => null)
|
|
44
|
+
const ranges = res && Array.isArray(res.data) ? decodeSemanticTokens(res.data) : []
|
|
45
|
+
cache = { path, version, ranges, at: Date.now() }
|
|
46
|
+
return ranges
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ed.onMouseMove((event) => {
|
|
50
|
+
const native = event?.event
|
|
51
|
+
const ctrl = native?.ctrlKey ?? false
|
|
52
|
+
const meta = native?.metaKey ?? false
|
|
53
|
+
if (!ctrl && !meta) {
|
|
54
|
+
clear()
|
|
55
|
+
hoverSeq++
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
const pos = event?.target?.position
|
|
59
|
+
const model = ed.getModel()
|
|
60
|
+
if (!pos || !model) {
|
|
61
|
+
clear()
|
|
62
|
+
hoverSeq++
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
const word = model.getWordAtPosition(pos)
|
|
66
|
+
if (!word) {
|
|
67
|
+
clear()
|
|
68
|
+
hoverSeq++
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
const seq = ++hoverSeq
|
|
72
|
+
const path = pathOfModel(model)
|
|
73
|
+
void rangesOf(model).then((ranges) => {
|
|
74
|
+
if (seq !== hoverSeq) return // 鼠标已移走,忽略过期结果
|
|
75
|
+
const lineIndex = Math.max(0, word.startLineNumber - 1)
|
|
76
|
+
const startCol = Math.max(0, word.startColumn - 1)
|
|
77
|
+
const endCol = Math.max(startCol, word.endColumn - 1)
|
|
78
|
+
const hit = ranges.some((r) => {
|
|
79
|
+
if (r.start.line !== lineIndex) return false
|
|
80
|
+
return isNavigableTokenType(r.type) && r.start.character <= startCol && r.end.character >= endCol
|
|
81
|
+
})
|
|
82
|
+
if (!hit) {
|
|
83
|
+
if (lastKey !== null) clear()
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
const key = String(path) + ':' + word.startColumn + ':' + word.endColumn
|
|
87
|
+
if (key === lastKey) return
|
|
88
|
+
lastKey = key
|
|
89
|
+
decoIds = ed.deltaDecorations(decoIds, [{
|
|
90
|
+
range: new monaco.Range(word.startLineNumber, word.startColumn, word.endLineNumber, word.endColumn),
|
|
91
|
+
options: { inlineClassName: 'edrv-nav-underline' },
|
|
92
|
+
}])
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
ed.onMouseLeave(() => clear())
|
|
97
|
+
ed.onDidBlurEditorText?.(() => clear())
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 兼容占位(与 providers 的 hideReferencesOverlay 对称,无需真正释放)。 */
|
|
101
|
+
export function disposeLspUnderline() {}
|
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
[data-edrv-view] .edrv-lsp-status-dot.ready { background: var(--dsw-alias-state-success-primary, #0f9d58); }
|
|
41
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
42
|
@keyframes edrv-lsp-pulse { 50% { opacity: .45; } }
|
|
43
|
+
/* Ctrl+hover 可导航标识符下划线提示(绑定 Monaco inlineClassName 装饰) */
|
|
44
|
+
[data-edrv-view] .edrv-nav-underline { text-decoration: underline; text-decoration-color: var(--dsw-alias-interactive-primary, #4fc1ff); text-underline-offset: 1px; cursor: pointer; }
|
|
43
45
|
[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; }
|
|
44
46
|
[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; }
|
|
45
47
|
[data-edrv-view] .edrv-chip-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.06)); }
|
|
@@ -26,6 +26,7 @@ import { bindingsOf, chordOf, matchEvent, useKeybindingsVersion } from '../keybi
|
|
|
26
26
|
import { createNavHistory } from '../navHistory.js'
|
|
27
27
|
import { CACHE_KEY } from '../paths.js'
|
|
28
28
|
import { bindLspEditor, runGoToDefinition, runFindReferences, hideReferencesOverlay } from '../monaco/lsp/providers.js'
|
|
29
|
+
import { bindLspUnderline } from '../monaco/lsp/underline.js'
|
|
29
30
|
import { onLspProgress, refreshStatus } from '../monaco/lsp/index.js'
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -841,6 +842,7 @@ export function EditorView(props) {
|
|
|
841
842
|
})
|
|
842
843
|
ed.addCommand(m.KeyCode.F12, () => { void runGoToDefinition(ed) })
|
|
843
844
|
bindLspEditor(ed)
|
|
845
|
+
bindLspUnderline(ed, m)
|
|
844
846
|
// hover 差异块 → 浮出 Keep/Undo(req:鼠标移到编辑区差异块时显示)
|
|
845
847
|
// 防闪烁:① 区域不变不 setState(浮窗锚定差异块起始行,不跟随鼠标);② 延迟隐藏;
|
|
846
848
|
// ③ 浮窗自身 onMouseEnter 取消隐藏计时(鼠标在浮窗与编辑器间移动不闪)。
|
package/src/lsp/client.ts
CHANGED
|
@@ -56,6 +56,7 @@ export function createLspClient(
|
|
|
56
56
|
const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }>()
|
|
57
57
|
let serverCapabilities: LspServerCapabilities = {
|
|
58
58
|
definition: false,
|
|
59
|
+
declaration: false,
|
|
59
60
|
references: false,
|
|
60
61
|
documentSymbol: false,
|
|
61
62
|
workspaceSymbol: false,
|
|
@@ -158,6 +159,7 @@ export function createLspClient(
|
|
|
158
159
|
const init = await client.request<{
|
|
159
160
|
capabilities?: {
|
|
160
161
|
definitionProvider?: unknown
|
|
162
|
+
declarationProvider?: unknown
|
|
161
163
|
referencesProvider?: unknown
|
|
162
164
|
documentSymbolProvider?: unknown
|
|
163
165
|
workspaceSymbolProvider?: unknown
|
|
@@ -179,6 +181,7 @@ export function createLspClient(
|
|
|
179
181
|
textDocument: {
|
|
180
182
|
synchronization: { dynamicRegistration: false, willSave: false, didSave: true },
|
|
181
183
|
definition: { dynamicRegistration: false },
|
|
184
|
+
declaration: { dynamicRegistration: false },
|
|
182
185
|
references: { dynamicRegistration: false },
|
|
183
186
|
documentSymbol: { dynamicRegistration: false },
|
|
184
187
|
hover: { dynamicRegistration: false },
|
|
@@ -196,6 +199,7 @@ export function createLspClient(
|
|
|
196
199
|
const legend = caps.semanticTokensProvider?.legend
|
|
197
200
|
serverCapabilities = {
|
|
198
201
|
definition: Boolean(caps.definitionProvider),
|
|
202
|
+
declaration: Boolean(caps.declarationProvider),
|
|
199
203
|
references: Boolean(caps.referencesProvider),
|
|
200
204
|
documentSymbol: Boolean(caps.documentSymbolProvider),
|
|
201
205
|
workspaceSymbol: Boolean(caps.workspaceSymbolProvider),
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — 定义查找降级链的纯推导函数。
|
|
3
|
+
* 背景:EmmyLua 对局部变量/参数/表字段的 textDocument/definition 常返回空,
|
|
4
|
+
* 但 references(includeDeclaration) 能给出包含声明的条目。
|
|
5
|
+
* 本函数从引用列表推导"声明位置":同文件、词文本与点击处一致,且点击位置落在某条目内时,
|
|
6
|
+
* 取列表首个(EmmyLua 对局部/参数把声明排在引用首位;点击处即首个时按"点击了声明"处理)。
|
|
7
|
+
* 词文本必须与点击词一致,否则拒绝(宁可空,不误跳)。
|
|
8
|
+
* 作者 ddj 2026-09-02
|
|
9
|
+
*/
|
|
10
|
+
import type { LspLocation, LspPosition, LspRange } from '../shared/lsp.js'
|
|
11
|
+
|
|
12
|
+
/** file:// URI(或裸路径)→ 归一化绝对路径(小写、/ 分隔)。 */
|
|
13
|
+
function normAbs(uri: string): string {
|
|
14
|
+
const path = String(uri ?? '').replace(/^file:\/\/\/?/, '')
|
|
15
|
+
return decodeURIComponent(path).replace(/\\/g, '/').replace(/^\/([A-Za-z]:)/, '$1').toLowerCase()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 取文档 text 在 LSP range(0-based)内的切片。 */
|
|
19
|
+
function sliceRange(text: string, range: LspRange): string {
|
|
20
|
+
const lines = text.split(/\r\n|\r|\n/)
|
|
21
|
+
const start = range.start
|
|
22
|
+
const end = range.end
|
|
23
|
+
const startLine = Math.max(0, start.line)
|
|
24
|
+
const endLine = Math.max(startLine, end.line)
|
|
25
|
+
if (startLine >= lines.length || endLine >= lines.length) return ''
|
|
26
|
+
const startChar = Math.max(0, start.character)
|
|
27
|
+
if (startLine === endLine) {
|
|
28
|
+
return lines[startLine].slice(startChar, Math.max(startChar, end.character))
|
|
29
|
+
}
|
|
30
|
+
const first = lines[startLine].slice(startChar)
|
|
31
|
+
const middle = lines.slice(startLine + 1, endLine)
|
|
32
|
+
const last = lines[endLine].slice(0, Math.max(0, end.character))
|
|
33
|
+
return [first, ...middle, last].join('\n')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** LSP position 是否落在 range 内(start 包含、end 排除;零宽按点匹配)。 */
|
|
37
|
+
function posInLspRange(pos: LspPosition, range: LspRange): boolean {
|
|
38
|
+
const start = range.start
|
|
39
|
+
const end = range.end ?? start
|
|
40
|
+
if (start.line === end.line && start.character === end.character) {
|
|
41
|
+
return pos.line === start.line && pos.character === start.character
|
|
42
|
+
}
|
|
43
|
+
if (pos.line < start.line || pos.line > end.line) return false
|
|
44
|
+
if (pos.line === start.line && pos.character < start.character) return false
|
|
45
|
+
if (pos.line === end.line && pos.character >= end.character) return false
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 两个 range 是否完全一致。 */
|
|
50
|
+
function sameRange(a: LspRange, b: LspRange): boolean {
|
|
51
|
+
return a.start.line === b.start.line && a.start.character === b.start.character &&
|
|
52
|
+
a.end.line === b.end.line && a.end.character === b.end.character
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** range 长度(行/列均按 0-based 差估算,仅用于取"最小包裹"排序)。 */
|
|
56
|
+
function rangeSize(r: LspRange): number {
|
|
57
|
+
return (r.end.line - r.start.line) * 1_000_000 + (r.end.line === r.start.line ? r.end.character - r.start.character : r.end.character)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 从引用列表推导定义位置。
|
|
62
|
+
* @author ddj 2026年09月02号
|
|
63
|
+
* @param queryPos 查询位置(LSP 0-based)
|
|
64
|
+
* @param locations references(includeDeclaration) 结果
|
|
65
|
+
* @param docText 当前文档全文(词文本一致校验)
|
|
66
|
+
* @param docAbsPath 当前文档绝对路径(root+相对路径,与 loc.uri 归一化后比较)
|
|
67
|
+
* @returns 推导出的定义位置;无法确定返回 null
|
|
68
|
+
*/
|
|
69
|
+
export function deriveDefinitionFromLocations(
|
|
70
|
+
queryPos: LspPosition,
|
|
71
|
+
locations: LspLocation[],
|
|
72
|
+
docText: string,
|
|
73
|
+
docAbsPath: string,
|
|
74
|
+
): LspLocation | null {
|
|
75
|
+
if (!locations.length) return null
|
|
76
|
+
const docAbs = normAbs(docAbsPath)
|
|
77
|
+
|
|
78
|
+
// 点击位置落在的条目(多个时取最小包裹 = 最贴近词的 range)
|
|
79
|
+
const containing = locations
|
|
80
|
+
.filter((loc) => posInLspRange(queryPos, loc.range))
|
|
81
|
+
.sort((a, b) => rangeSize(a.range) - rangeSize(b.range))[0]
|
|
82
|
+
if (!containing) return null
|
|
83
|
+
const queryWord = sliceRange(docText, containing.range)
|
|
84
|
+
if (!queryWord) return null
|
|
85
|
+
|
|
86
|
+
// 同文件 + 词文本一致的候选(按服务器返回顺序)
|
|
87
|
+
const candidates = locations.filter((loc) => {
|
|
88
|
+
return normAbs(loc.uri) === docAbs && sliceRange(docText, loc.range) === queryWord
|
|
89
|
+
})
|
|
90
|
+
if (!candidates.length) return null
|
|
91
|
+
|
|
92
|
+
// 点击处即候选首个(且与查询词重合)→ 点击的就是声明,返回自身
|
|
93
|
+
// 否则取首个(EmmyLua 对局部/参数把声明排在引用首位)
|
|
94
|
+
const first = candidates[0]
|
|
95
|
+
return sameRange(first.range, containing.range) ? containing : first
|
|
96
|
+
}
|
package/src/lsp/server.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { spawnServer, type Transport } from './transport.js'
|
|
8
8
|
import { createLspClient, type LspClient } from './client.js'
|
|
9
9
|
import { pathToFileUri } from './uri.js'
|
|
10
|
+
import { deriveDefinitionFromLocations } from './derive.js'
|
|
10
11
|
import type { LspProviderSpec } from './providers.js'
|
|
11
12
|
import { LSP_SEMANTIC_TOKEN_MODIFIERS, LSP_SEMANTIC_TOKEN_TYPES } from '../shared/lsp.js'
|
|
12
13
|
import type {
|
|
@@ -64,6 +65,7 @@ export function createLspServer(spec: LspProviderSpec, root: string, languageId:
|
|
|
64
65
|
let client: LspClient | null = null
|
|
65
66
|
let capabilities: LspServerCapabilities = {
|
|
66
67
|
definition: false,
|
|
68
|
+
declaration: false,
|
|
67
69
|
references: false,
|
|
68
70
|
documentSymbol: false,
|
|
69
71
|
workspaceSymbol: false,
|
|
@@ -231,11 +233,55 @@ export function createLspServer(spec: LspProviderSpec, root: string, languageId:
|
|
|
231
233
|
async definition(path: string, line: number, character: number): Promise<LspLocation[]> {
|
|
232
234
|
const doc = docs.get(path)
|
|
233
235
|
if (!doc || !(await readyWait)) return []
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
236
|
+
const client = ensureClient()
|
|
237
|
+
const position = { line, character }
|
|
238
|
+
// ① 标准定义(方法/跨文件命名空间等)
|
|
239
|
+
const direct = normalizeLocations(
|
|
240
|
+
await client.request<unknown>(
|
|
241
|
+
'textDocument/definition',
|
|
242
|
+
{ textDocument: { uri: doc.uri }, position },
|
|
243
|
+
),
|
|
237
244
|
)
|
|
238
|
-
|
|
245
|
+
if (direct.length) return direct
|
|
246
|
+
// ② 声明(服务器能力支持时):EmmyLua 对局部变量/参数/表字段 definition 常返回空,declaration 更准
|
|
247
|
+
if (capabilities.declaration) {
|
|
248
|
+
try {
|
|
249
|
+
const decl = normalizeLocations(
|
|
250
|
+
await client.request<unknown>(
|
|
251
|
+
'textDocument/declaration',
|
|
252
|
+
{ textDocument: { uri: doc.uri }, position },
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
if (decl.length) {
|
|
256
|
+
log('definition fallback → declaration (' + path + ':' + line + ':' + character + ')')
|
|
257
|
+
return decl
|
|
258
|
+
}
|
|
259
|
+
} catch (error) {
|
|
260
|
+
log('declaration request failed: ' + String(error))
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// ③ 引用推导:definition/declaration 均空时,从 references(includeDeclaration) 推导声明
|
|
264
|
+
// (EmmyLua 局部/参数把声明排在引用首位;词文本不一致则拒绝,宁可空不误跳)
|
|
265
|
+
try {
|
|
266
|
+
const refs = normalizeLocations(
|
|
267
|
+
await client.request<unknown>(
|
|
268
|
+
'textDocument/references',
|
|
269
|
+
{
|
|
270
|
+
textDocument: { uri: doc.uri },
|
|
271
|
+
position,
|
|
272
|
+
context: { includeDeclaration: true },
|
|
273
|
+
},
|
|
274
|
+
),
|
|
275
|
+
)
|
|
276
|
+
const derived = deriveDefinitionFromLocations(position, refs, doc.text, doc.uri)
|
|
277
|
+
if (derived) {
|
|
278
|
+
log('definition fallback → references-derived (' + path + ':' + line + ':' + character + ')')
|
|
279
|
+
return [derived]
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
log('references fallback failed: ' + String(error))
|
|
283
|
+
}
|
|
284
|
+
return []
|
|
239
285
|
},
|
|
240
286
|
|
|
241
287
|
async references(path: string, line: number, character: number, includeDeclaration: boolean): Promise<LspLocation[]> {
|
package/src/shared/lsp.ts
CHANGED
|
@@ -69,9 +69,66 @@ export interface LspSemanticTokens {
|
|
|
69
69
|
resultId?: string
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/** 语义 token 解码后的绝对范围(LSP 0-based;type 为插件固定 legend 与 type 名)。 */
|
|
73
|
+
export interface LspSemanticTokenRange {
|
|
74
|
+
start: LspPosition
|
|
75
|
+
end: LspPosition
|
|
76
|
+
type: string
|
|
77
|
+
modifiers: number
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 语义 token 中可作为"可导航标识符"的类型(ctrl+hover 下划线判定用)。 */
|
|
81
|
+
export const LSP_NAVIGABLE_TOKEN_TYPES: ReadonlySet<string> = new Set([
|
|
82
|
+
'variable', 'parameter', 'property', 'function', 'method', 'class', 'enum',
|
|
83
|
+
'struct', 'interface', 'namespace', 'typeParameter', 'enumMember', 'event', 'type',
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 语义 token 是否属于可导航标识符。
|
|
88
|
+
* @author ddj 2026年09月02号
|
|
89
|
+
* @param type 语义 token 类型名(插件固定 legend)
|
|
90
|
+
* @returns 是否可导航
|
|
91
|
+
*/
|
|
92
|
+
export function isNavigableTokenType(type: string): boolean {
|
|
93
|
+
return LSP_NAVIGABLE_TOKEN_TYPES.has(type)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 解码 LSP delta 编码的语义 token 流为绝对范围数组。
|
|
98
|
+
* @author ddj 2026年09月02号
|
|
99
|
+
* @param data 5 元组 delta 流(host normalizeSemanticData 产物,类型索引对应固定 legend)
|
|
100
|
+
* @param types 固定 legend 类型表(缺省 LSP_SEMANTIC_TOKEN_TYPES)
|
|
101
|
+
* @returns 绝对范围列表(保持文档顺序)
|
|
102
|
+
*/
|
|
103
|
+
export function decodeSemanticTokens(
|
|
104
|
+
data: number[],
|
|
105
|
+
types: readonly string[] = LSP_SEMANTIC_TOKEN_TYPES,
|
|
106
|
+
): LspSemanticTokenRange[] {
|
|
107
|
+
const out: LspSemanticTokenRange[] = []
|
|
108
|
+
let line = 0
|
|
109
|
+
let character = 0
|
|
110
|
+
for (let i = 0; i + 4 < data.length; i += 5) {
|
|
111
|
+
const deltaLine = data[i] ?? 0
|
|
112
|
+
const deltaCharacter = data[i + 1] ?? 0
|
|
113
|
+
const length = Math.max(0, data[i + 2] ?? 0)
|
|
114
|
+
const typeIndex = data[i + 3] ?? 0
|
|
115
|
+
const modifiers = data[i + 4] ?? 0
|
|
116
|
+
line += deltaLine
|
|
117
|
+
character = deltaLine === 0 ? character + deltaCharacter : deltaCharacter
|
|
118
|
+
out.push({
|
|
119
|
+
start: { line, character },
|
|
120
|
+
end: { line, character: character + length },
|
|
121
|
+
type: types[typeIndex] ?? 'variable',
|
|
122
|
+
modifiers,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
return out
|
|
126
|
+
}
|
|
127
|
+
|
|
72
128
|
/** LSP 服务器能力子集:本插件会用到的方法。 */
|
|
73
129
|
export interface LspServerCapabilities {
|
|
74
130
|
definition: boolean
|
|
131
|
+
declaration: boolean
|
|
75
132
|
references: boolean
|
|
76
133
|
documentSymbol: boolean
|
|
77
134
|
workspaceSymbol: boolean
|