dsh-vscode-mode 0.9.0 → 0.11.0
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 +42 -3
- package/lib/client.js +1587 -301
- package/lib/client.js.map +1 -1
- package/lib/index.js +2163 -1042
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/addToConversation.ts +74 -28
- package/src/client/dap/hover.ts +2 -7
- package/src/client/index.ts +25 -0
- package/src/client/luaReservedWords.ts +10 -0
- package/src/client/monaco/lsp/keywords.ts +121 -0
- package/src/client/monaco/lsp/lspClient.ts +68 -6
- package/src/client/monaco/lsp/memberScope.ts +90 -0
- package/src/client/monaco/lsp/providers.ts +14 -4
- package/src/client/settingsDocRouter.ts +52 -0
- package/src/client/sidebar/menuItems.ts +1 -0
- package/src/client/sidebar/panels/SvnPanel.ts +1 -1
- package/src/client/sidebar/types.ts +2 -0
- package/src/client/styles/editor.css +143 -37
- package/src/client/svnActions.ts +4 -4
- package/src/client/ui/ContextMenu.ts +4 -2
- package/src/client/ui/EditorView.ts +132 -52
- package/src/client/ui/SvnLogDialog.ts +200 -69
- package/src/client/ui/SvnUpdateDialog.ts +472 -0
- package/src/client/ui/svnUpdateModel.ts +135 -0
- package/src/index.ts +2 -0
- package/src/lsp/completionCap.ts +80 -0
- package/src/lsp/manager.ts +42 -10
- package/src/lsp/rpc.ts +8 -5
- package/src/lsp/server.ts +18 -4
- package/src/rpc.ts +74 -2
- package/src/settingsDocPath.ts +30 -0
- package/src/shared/rpc.ts +12 -2
- package/src/shared/svn.ts +38 -0
- package/src/svn.ts +102 -7
- package/src/svnText.ts +22 -13
- package/src/svnUpdateJob.ts +588 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"icon": "assets/icon.svg",
|
|
6
6
|
"keywords": [
|
|
@@ -285,6 +285,17 @@ function mintRpcId(): string {
|
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
/**
|
|
289
|
+
* wire 槽实参组装(gateway `invoke` 铁证:`request.args[parameter.wire]` 具名槽,
|
|
290
|
+
* `session/prompt` 的槽名 = "request",值 = RpcRequest `{rpcId, payload}`)。
|
|
291
|
+
* @author ddj 2026年09月24号
|
|
292
|
+
* @param payload 业务载荷
|
|
293
|
+
* @returns 具名槽实参 `{ args: { request: RpcRequest } }`
|
|
294
|
+
*/
|
|
295
|
+
function slotArg(payload: object): { args: { request: { rpcId: string; payload: object } } } {
|
|
296
|
+
return { args: { request: { rpcId: mintRpcId(), payload } } }
|
|
297
|
+
}
|
|
298
|
+
|
|
288
299
|
/**
|
|
289
300
|
* RPC 返回解析(照官方 dsh-client-runtime 调用形态:`const { result } = await api.sessions.create(...)`)。
|
|
290
301
|
* 官方 api.sessions 面返回**信封** `{ rpcId, result: RpcResult }`——须解 .result;
|
|
@@ -335,6 +346,7 @@ async function apiSessionCreate(cwd: string | undefined): Promise<string> {
|
|
|
335
346
|
*/
|
|
336
347
|
export async function apiSessionPrompt(sessionId: string, text: string): Promise<boolean> {
|
|
337
348
|
try {
|
|
349
|
+
const payload = { sessionId, mode: 'queue', content: [{ type: 'text', text }] }
|
|
338
350
|
const res = await fetch(SESSION_PROMPT_PATH, {
|
|
339
351
|
method: 'POST',
|
|
340
352
|
headers: { 'content-type': 'application/json' },
|
|
@@ -342,7 +354,7 @@ export async function apiSessionPrompt(sessionId: string, text: string): Promise
|
|
|
342
354
|
type: 'client-request',
|
|
343
355
|
rpcId: mintRpcId(),
|
|
344
356
|
method: 'session/prompt',
|
|
345
|
-
payload:
|
|
357
|
+
payload: slotArg(payload),
|
|
346
358
|
}),
|
|
347
359
|
})
|
|
348
360
|
const json = await res.json().catch(() => null) as { result?: { ok?: boolean } } | null
|
|
@@ -362,25 +374,10 @@ export async function apiSessionPrompt(sessionId: string, text: string): Promise
|
|
|
362
374
|
* @returns 新会话结果(含 id)
|
|
363
375
|
*/
|
|
364
376
|
export async function newDraftSession(ctx: CtxLike, cwd: string | undefined): Promise<DraftSessionResult> {
|
|
365
|
-
// 主路径:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (id) return { ok: true, id }
|
|
369
|
-
} catch { /* wire 不可用:回落 service 面 */ }
|
|
377
|
+
// 主路径:client store manager 面(官方 connectWorkspace 同链路)——create 自带 store upsert,
|
|
378
|
+
// 「on resolution the child is in the list store and open() can target it」;
|
|
379
|
+
// wire 直调虽能建会话但 store 无感,故只作最后兜底。
|
|
370
380
|
try {
|
|
371
|
-
// 回落 1:remote.session service 面(官方返回信封 {rpcId, result},见 rpcOk;实参双形态兼容)
|
|
372
|
-
const remote = ctx.get('remote.session') as {
|
|
373
|
-
create?: (req: unknown) => Promise<unknown>
|
|
374
|
-
} | undefined
|
|
375
|
-
if (typeof remote?.create === 'function') {
|
|
376
|
-
const payload = cwd ? { cwd } : {}
|
|
377
|
-
for (const arg of [payload, { rpcId: mintRpcId(), payload }]) {
|
|
378
|
-
const r = rpcOk(await remote.create(arg))
|
|
379
|
-
const id = r.value as { sessionId?: unknown } | undefined
|
|
380
|
-
if (r.ok && typeof id?.sessionId === 'string') return { ok: true, id: id.sessionId }
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
// 回落 2:旧版 client store 形态(id / {id} / {sessionId} / {session:{id}} 四解)
|
|
384
381
|
const sessions = ctx.get('sessions') as { create?: (opts?: { cwd?: string }) => unknown } | undefined
|
|
385
382
|
if (typeof sessions?.create === 'function') {
|
|
386
383
|
const created = await sessions.create(cwd ? { cwd } : undefined)
|
|
@@ -392,6 +389,25 @@ export async function newDraftSession(ctx: CtxLike, cwd: string | undefined): Pr
|
|
|
392
389
|
: ''
|
|
393
390
|
if (id) return { ok: true, id }
|
|
394
391
|
}
|
|
392
|
+
} catch { /* 回落下一通道 */ }
|
|
393
|
+
try {
|
|
394
|
+
// 回落 1:api.sessions service 面(返回信封 {rpcId, result},见 rpcOk;多形态容错)
|
|
395
|
+
const remote = ctx.get('remote.session') as { create?: (req: unknown) => Promise<unknown> } | undefined
|
|
396
|
+
if (typeof remote?.create === 'function') {
|
|
397
|
+
const payload = cwd ? { cwd } : {}
|
|
398
|
+
for (const arg of [slotArg(payload), { request: { rpcId: mintRpcId(), payload } }, { rpcId: mintRpcId(), payload }, payload]) {
|
|
399
|
+
try {
|
|
400
|
+
const r = rpcOk(await remote.create(arg))
|
|
401
|
+
const id = r.value as { sessionId?: unknown } | undefined
|
|
402
|
+
if (r.ok && typeof id?.sessionId === 'string') return { ok: true, id: id.sessionId }
|
|
403
|
+
} catch { /* 单形态失败试下一形态 */ }
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} catch { /* 回落 wire */ }
|
|
407
|
+
// 回落 2:wire 直调(创建成功但 store 无感,open 切换不保证;任务发送仍可用)
|
|
408
|
+
try {
|
|
409
|
+
const id = await apiSessionCreate(cwd)
|
|
410
|
+
if (id) return { ok: true, id }
|
|
395
411
|
} catch { /* 全部失败:调用方降级 */ }
|
|
396
412
|
return { ok: false }
|
|
397
413
|
}
|
|
@@ -559,11 +575,21 @@ export function createAddToConversation(ctx: CtxLike): AddToConversation {
|
|
|
559
575
|
const startDraftSession: AddToConversation['startDraftSession'] = async (cwd) => {
|
|
560
576
|
const result = await newDraftSession(ctx, cwd)
|
|
561
577
|
if (result.ok && result.id) {
|
|
562
|
-
// 尽力切换到新会话(缺失/异常不影响结果——仅视图不切换)
|
|
563
578
|
try {
|
|
564
|
-
|
|
565
|
-
sessions
|
|
566
|
-
|
|
579
|
+
// 主动铸造 scope(scope 对新会话惰性铸造,inputFor/composer 面依赖它)
|
|
580
|
+
const sessions = ctx.get('sessions') as {
|
|
581
|
+
materializeScope?: (id: string) => unknown
|
|
582
|
+
manager?: { select?: (id: string) => void; rename?: (id: string, title: string) => void }
|
|
583
|
+
open?: (id: string) => void
|
|
584
|
+
} | undefined
|
|
585
|
+
sessions?.materializeScope?.(result.id)
|
|
586
|
+
// 命名醒目标题(manager.rename 在面实证):会话列表一眼可见、点击即入
|
|
587
|
+
const title = 'AI 深度分析 · ' + (String(cwd ?? '').split(/[\\/]/).pop() || 'SVN 变更')
|
|
588
|
+
void sessions?.manager?.rename?.(result.id, title)
|
|
589
|
+
// 切换尝试(open/select 未对插件暴露时静默跳过:rename 已保证可见性)
|
|
590
|
+
void sessions?.manager?.select?.(result.id)
|
|
591
|
+
void sessions?.open?.(result.id)
|
|
592
|
+
} catch { /* 面能力缺失不致命:任务发送不受影响 */ }
|
|
567
593
|
}
|
|
568
594
|
return result
|
|
569
595
|
}
|
|
@@ -578,18 +604,38 @@ export function createAddToConversation(ctx: CtxLike): AddToConversation {
|
|
|
578
604
|
*/
|
|
579
605
|
const sendTask: AddToConversation['sendTask'] = async (sessionId, text) => {
|
|
580
606
|
if (!sessionId) return false
|
|
607
|
+
// 主路径:**官方 composer 发送链路**(InputLike setDraft + submit —— UI「发送」的内部路径)。
|
|
608
|
+
// inputFor 依赖 sessions.scope(id):新会话铸造有时延,重试 3 次后再降级。
|
|
609
|
+
const composerOnce = async (): Promise<boolean> => {
|
|
610
|
+
const input = inputFor(ctx, sessionId) as { setDraft?: (t: string) => void; submit?: (mode?: unknown) => void } | undefined
|
|
611
|
+
if (input && typeof input.setDraft === 'function' && typeof input.submit === 'function') {
|
|
612
|
+
input.setDraft(text)
|
|
613
|
+
input.submit()
|
|
614
|
+
return true
|
|
615
|
+
}
|
|
616
|
+
return false
|
|
617
|
+
}
|
|
581
618
|
try {
|
|
582
|
-
|
|
583
|
-
|
|
619
|
+
for (let attempt = 3; attempt > 0; attempt--) {
|
|
620
|
+
if (await composerOnce()) return true
|
|
621
|
+
if (attempt > 1) await new Promise((resolve) => setTimeout(resolve, 500))
|
|
622
|
+
}
|
|
623
|
+
} catch { /* 降级下一通道 */ }
|
|
624
|
+
// 兜底 1:remote.session service 面(多形态容错)
|
|
584
625
|
try {
|
|
585
626
|
const remote = ctx.get('remote.session') as { prompt?: (req: unknown) => Promise<unknown> } | undefined
|
|
586
627
|
if (typeof remote?.prompt === 'function') {
|
|
587
|
-
// 官方 api.sessions.prompt(request) 返回信封(rpcOk 解 .result);实参双形态兼容
|
|
588
628
|
const payload = { sessionId, mode: 'queue', content: [{ type: 'text', text }] }
|
|
589
|
-
for (const arg of [payload, { rpcId: mintRpcId(), payload }]) {
|
|
590
|
-
|
|
629
|
+
for (const arg of [slotArg(payload), { request: { rpcId: mintRpcId(), payload } }, { rpcId: mintRpcId(), payload }, payload]) {
|
|
630
|
+
try {
|
|
631
|
+
if (rpcOk(await remote.prompt(arg)).ok) return true
|
|
632
|
+
} catch { /* 单形态失败试下一形态 */ }
|
|
591
633
|
}
|
|
592
634
|
}
|
|
635
|
+
} catch { /* 降级下一通道 */ }
|
|
636
|
+
// 兜底 2:HTTP wire
|
|
637
|
+
try {
|
|
638
|
+
return await apiSessionPrompt(sessionId, text)
|
|
593
639
|
} catch { /* 全失败 */ }
|
|
594
640
|
return false
|
|
595
641
|
}
|
package/src/client/dap/hover.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { dapStore } from './store.js'
|
|
9
9
|
import { pathOfModel } from '../monaco/lsp/lspClient.js'
|
|
10
|
+
import { LUA_RESERVED_WORD_SET } from '../luaReservedWords.js'
|
|
10
11
|
import { BLOCK_ATTR, EQ_ATTR, EXPR_ATTR, HEAD_ATTR, TYPE_ATTR, VAL_ATTR, createTreeState, escapeHtml, initialHtml } from './hoverTree.js'
|
|
11
12
|
|
|
12
13
|
const GLOBAL_KEY = '__edrvDapHoverProvider__'
|
|
@@ -35,12 +36,6 @@ function treeKeyOf(target: { expression: string; lineNumber: number; startColumn
|
|
|
35
36
|
return target.expression + '@' + path + ':' + target.lineNumber + ':' + target.startColumn + '-' + target.endColumn
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
/** Lua 保留字集合(保留字不是可求值表达式,避免 adapter 返回 syntax err 噪声)。 */
|
|
39
|
-
const LUA_KEYWORDS = new Set([
|
|
40
|
-
'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'goto',
|
|
41
|
-
'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while',
|
|
42
|
-
])
|
|
43
|
-
|
|
44
39
|
/** 可求值路径表达式:标识符链,成员用 . 或 : 连接(注释里的中文文本等一律不匹配)。 */
|
|
45
40
|
const EVAL_EXPR = /^[A-Za-z_][A-Za-z0-9_]*(?:[.:][A-Za-z_][A-Za-z0-9_]*)*$/
|
|
46
41
|
|
|
@@ -51,7 +46,7 @@ const EVAL_EXPR = /^[A-Za-z_][A-Za-z0-9_]*(?:[.:][A-Za-z_][A-Za-z0-9_]*)*$/
|
|
|
51
46
|
* @returns 是否可求值
|
|
52
47
|
*/
|
|
53
48
|
function isEvalExpr(text: string): boolean {
|
|
54
|
-
return EVAL_EXPR.test(text) && !
|
|
49
|
+
return EVAL_EXPR.test(text) && !LUA_RESERVED_WORD_SET.has(text)
|
|
55
50
|
}
|
|
56
51
|
|
|
57
52
|
/**
|
package/src/client/index.ts
CHANGED
|
@@ -27,6 +27,8 @@ import { createFileOpenerRegistry, scanSidebar, officialSidebarOpener, shouldCla
|
|
|
27
27
|
import type { FileOpenerRegistry } from './fileOpeners.js'
|
|
28
28
|
import { installOpenPathRouter, vscodeOpener, autoValue } from './openPathRouter.js'
|
|
29
29
|
import { patchRemoteOpen, probeRemoteOpen } from './remoteOpenRouter.js'
|
|
30
|
+
import { patchSettingsDoc, probeSettingsDoc } from './settingsDocRouter.js'
|
|
31
|
+
import { openEditorView } from './events.js'
|
|
30
32
|
import { setupExtOpen } from './externalOpen.js'
|
|
31
33
|
import { SettingsContext } from './settingsContext.js'
|
|
32
34
|
import { SIDEBAR_PLUGIN, registerSlotSafely, settingsBridge } from './compat.js'
|
|
@@ -104,6 +106,29 @@ function setupCommands(ctx: any): void {
|
|
|
104
106
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
107
|
export function apply(ctx: any): void {
|
|
106
108
|
const schedule = (fn: () => void, ms: number) => ctx.timeout(fn, ms)
|
|
109
|
+
ctx.effect(() => {
|
|
110
|
+
let disposed = false
|
|
111
|
+
let attempts = 0
|
|
112
|
+
let restore: (() => void) | null = null
|
|
113
|
+
const install = (): void => {
|
|
114
|
+
if (disposed || restore || attempts >= 15) return
|
|
115
|
+
attempts += 1
|
|
116
|
+
const service = probeSettingsDoc(ctx)
|
|
117
|
+
if (service) {
|
|
118
|
+
restore = patchSettingsDoc(service, async () => {
|
|
119
|
+
const result = await rpc('vscode.settingsDocumentPath', {})
|
|
120
|
+
return result.ok ? { ok: true, path: result.path } : { ok: false, error: result.error }
|
|
121
|
+
}, openEditorView, (message) => log.warn(message))
|
|
122
|
+
if (restore) return
|
|
123
|
+
}
|
|
124
|
+
if (attempts < 15) schedule(install, 2000)
|
|
125
|
+
}
|
|
126
|
+
install()
|
|
127
|
+
return () => {
|
|
128
|
+
disposed = true
|
|
129
|
+
restore?.()
|
|
130
|
+
}
|
|
131
|
+
}, 'vscode-mode: settings document editor route')
|
|
107
132
|
|
|
108
133
|
// 指令系统(命令栏 + 指令注册表)先装配:命令栏在被任何 React slot 渲染前即可唤起
|
|
109
134
|
setupCommands(ctx)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lua 5.3 保留字,供编辑器补全与调试悬停共同使用。
|
|
3
|
+
* 作者 ddj 2026年09月24号
|
|
4
|
+
*/
|
|
5
|
+
export const LUA_RESERVED_WORDS = [
|
|
6
|
+
'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'goto',
|
|
7
|
+
'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while',
|
|
8
|
+
] as const
|
|
9
|
+
|
|
10
|
+
export const LUA_RESERVED_WORD_SET: ReadonlySet<string> = new Set(LUA_RESERVED_WORDS)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 为 Lua 的 LSP 补全补充服务器缺失的保留字与注解基础类型候选。
|
|
3
|
+
* 作者 ddj 2026年09月24号
|
|
4
|
+
*/
|
|
5
|
+
import { LUA_RESERVED_WORDS } from '../../luaReservedWords.js'
|
|
6
|
+
|
|
7
|
+
const LUA_BASE_TYPES = [
|
|
8
|
+
'any', 'boolean', 'integer', 'nil', 'number', 'string', 'table', 'thread', 'userdata',
|
|
9
|
+
] as const
|
|
10
|
+
const TYPE_START = /^\s*---@(?:(?:type|return)\s+|param\s+\S+\s+|field\s+(?:(?:public|private|protected|package)\s+)?\S+\s+)(.*)$/
|
|
11
|
+
|
|
12
|
+
interface KeywordRange {
|
|
13
|
+
startLineNumber: number
|
|
14
|
+
endLineNumber: number
|
|
15
|
+
startColumn: number
|
|
16
|
+
endColumn: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface KeywordItem {
|
|
20
|
+
label: string
|
|
21
|
+
kind: number
|
|
22
|
+
insertText: string
|
|
23
|
+
range: KeywordRange
|
|
24
|
+
detail: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 构造轻量本地候选,保留服务器同名项的优先权交给 mergeKeywords。
|
|
29
|
+
* @author ddj 2026年09月24号
|
|
30
|
+
* @param words 候选文本
|
|
31
|
+
* @param kind Monaco 候选类型
|
|
32
|
+
* @param range 当前单词范围
|
|
33
|
+
* @param detail 候选说明
|
|
34
|
+
* @returns 可直接交给 Monaco 的候选
|
|
35
|
+
*/
|
|
36
|
+
function keywordItems(words: readonly string[], kind: number, range: KeywordRange, detail: string): KeywordItem[] {
|
|
37
|
+
return words.map((label) => ({ label, kind, insertText: label, range, detail }))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 判断光标是否在 EmmyLua 注解的类型槽;普通注释和描述文字均不匹配。
|
|
42
|
+
* @author ddj 2026年09月24号
|
|
43
|
+
* @param model 当前文档
|
|
44
|
+
* @param position 光标所在行
|
|
45
|
+
* @param range 当前词范围
|
|
46
|
+
* @returns 是否应提示基础类型
|
|
47
|
+
*/
|
|
48
|
+
export function isLuaTypeSlot(
|
|
49
|
+
model: { getLineContent(lineNumber: number): string },
|
|
50
|
+
position: { lineNumber: number },
|
|
51
|
+
range: Pick<KeywordRange, 'startColumn'>,
|
|
52
|
+
): boolean {
|
|
53
|
+
const beforeWord = model.getLineContent(position.lineNumber).slice(0, range.startColumn - 1)
|
|
54
|
+
const match = TYPE_START.exec(beforeWord)
|
|
55
|
+
if (!match) return false
|
|
56
|
+
const context = match[1] ?? ''
|
|
57
|
+
const typePrefix = context.trimEnd()
|
|
58
|
+
if (/[.:]$/.test(typePrefix)) return false
|
|
59
|
+
return !(/\s$/.test(context) && typePrefix && !/[|&,<]$/.test(typePrefix))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 在普通代码位置生成关键字;成员名和类型注解位置只交给对应候选。
|
|
64
|
+
* @author ddj 2026年09月24号
|
|
65
|
+
* @param monaco Monaco 语言枚举
|
|
66
|
+
* @param model 当前文档
|
|
67
|
+
* @param position 光标所在行
|
|
68
|
+
* @param range 当前单词的替换范围
|
|
69
|
+
* @returns 当前上下文可用的 Lua 关键字候选
|
|
70
|
+
*/
|
|
71
|
+
export function luaKeywordsAt(
|
|
72
|
+
monaco: { languages: { CompletionItemKind: { Keyword: number } } },
|
|
73
|
+
model: { getLineContent(lineNumber: number): string },
|
|
74
|
+
position: { lineNumber: number },
|
|
75
|
+
range: KeywordRange,
|
|
76
|
+
): KeywordItem[] {
|
|
77
|
+
const line = model.getLineContent(position.lineNumber)
|
|
78
|
+
const beforeWord = line.charAt(range.startColumn - 2)
|
|
79
|
+
if (beforeWord === '.' || beforeWord === ':' || /^\s*---@/.test(line)) return []
|
|
80
|
+
return keywordItems(LUA_RESERVED_WORDS, monaco.languages.CompletionItemKind.Keyword, range, 'Lua 关键字')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 仅在注解类型槽补最基本类型,项目类型和服务器自带类型仍由 LSP 提供。
|
|
85
|
+
* @author ddj 2026年09月24号
|
|
86
|
+
* @param monaco Monaco 语言枚举
|
|
87
|
+
* @param model 当前文档
|
|
88
|
+
* @param position 光标所在行
|
|
89
|
+
* @param range 当前单词的替换范围
|
|
90
|
+
* @returns 类型候选;非注解返回空数组
|
|
91
|
+
*/
|
|
92
|
+
export function luaTypesAt(
|
|
93
|
+
monaco: { languages: { CompletionItemKind: { Keyword: number } } },
|
|
94
|
+
model: { getLineContent(lineNumber: number): string },
|
|
95
|
+
position: { lineNumber: number },
|
|
96
|
+
range: KeywordRange,
|
|
97
|
+
): KeywordItem[] {
|
|
98
|
+
if (!isLuaTypeSlot(model, position, range)) return []
|
|
99
|
+
return keywordItems(LUA_BASE_TYPES, monaco.languages.CompletionItemKind.Keyword, range, 'Lua 基础类型')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 合并 LSP 与本地关键字,服务器同名项优先并保持原顺序。
|
|
104
|
+
* @author ddj 2026年09月24号
|
|
105
|
+
* @param server 服务器候选
|
|
106
|
+
* @param keywords 本地关键字候选
|
|
107
|
+
* @returns 去重后的补全列表
|
|
108
|
+
*/
|
|
109
|
+
export function mergeKeywords<T extends { label: string }, U extends { label: string }>(
|
|
110
|
+
server: T[],
|
|
111
|
+
keywords: U[],
|
|
112
|
+
): Array<T | U> {
|
|
113
|
+
if (keywords.length === 0) return server
|
|
114
|
+
const labels = new Set(server.map((item) => item.label))
|
|
115
|
+
const merged: Array<T | U> = [...server]
|
|
116
|
+
for (const keyword of keywords) {
|
|
117
|
+
if (labels.has(keyword.label)) continue
|
|
118
|
+
merged.push(keyword)
|
|
119
|
+
}
|
|
120
|
+
return merged
|
|
121
|
+
}
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* 并提供 definition/references/documentSymbol/hover/status 的 RPC 薄封装。
|
|
6
6
|
* 作者 ddj 2026-08-27
|
|
7
7
|
*/
|
|
8
|
-
import { rpc } from '../../rpc.js'
|
|
8
|
+
import { rpc, dbg } from '../../rpc.js'
|
|
9
9
|
import { monoToLsp } from '../../../shared/lsp.js'
|
|
10
|
+
import { classifyMemberSet } from './memberScope.js'
|
|
10
11
|
|
|
11
12
|
/** edrv:// 模型 URI → 工作区相对/绝对路径(与 host 侧 path 对齐)。 */
|
|
12
13
|
export function pathOfModel(model) {
|
|
@@ -23,9 +24,11 @@ export function pathOfModel(model) {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const SYNC_DEBOUNCE_MS = 250
|
|
27
|
+
const MAX_DOC_VERSION = 2147483647
|
|
26
28
|
|
|
27
29
|
let sessionId
|
|
28
30
|
const syncTimers = new Map()
|
|
31
|
+
const docVersions = new Map()
|
|
29
32
|
let statusCache = { servers: [], at: 0 }
|
|
30
33
|
let progressListeners = new Set()
|
|
31
34
|
let ready = false
|
|
@@ -47,6 +50,9 @@ function reportStatus(text) {
|
|
|
47
50
|
*/
|
|
48
51
|
export function setLspSession(id) {
|
|
49
52
|
if (sessionId === id) return
|
|
53
|
+
for (const timer of syncTimers.values()) clearTimeout(timer)
|
|
54
|
+
syncTimers.clear()
|
|
55
|
+
docVersions.clear()
|
|
50
56
|
sessionId = id
|
|
51
57
|
void refreshStatus()
|
|
52
58
|
}
|
|
@@ -87,6 +93,18 @@ function publishProgress() {
|
|
|
87
93
|
}
|
|
88
94
|
}
|
|
89
95
|
|
|
96
|
+
/**
|
|
97
|
+
* LSP 版本须可被 Roslyn 的 Int32 反序列化;时间戳会直接使其退出。
|
|
98
|
+
* @author ddj 2026年09月24号
|
|
99
|
+
* @param path 文档路径
|
|
100
|
+
* @returns 当前文档下一个协议版本
|
|
101
|
+
*/
|
|
102
|
+
function nextDocVer(path) {
|
|
103
|
+
const version = Math.min(MAX_DOC_VERSION, (docVersions.get(path) ?? 0) + 1)
|
|
104
|
+
docVersions.set(path, version)
|
|
105
|
+
return version
|
|
106
|
+
}
|
|
107
|
+
|
|
90
108
|
/**
|
|
91
109
|
* 立即同步某文档到 host(首次打开/查询前保证服务器已认识该文档)。
|
|
92
110
|
* sessionId 缺失时仍发起请求:host 对唯一活跃会话有兜底解析,弃发会白白丢失文档归属。
|
|
@@ -99,13 +117,13 @@ export async function syncDoc(path, text, immediate = false) {
|
|
|
99
117
|
if (!path || typeof text !== 'string') return
|
|
100
118
|
if (immediate) {
|
|
101
119
|
clearPending(path)
|
|
102
|
-
await rpc('edrv.lsp.sync', { sessionId, path, text, version:
|
|
120
|
+
await rpc('edrv.lsp.sync', { sessionId, path, text, version: nextDocVer(path) }).catch(() => {})
|
|
103
121
|
return
|
|
104
122
|
}
|
|
105
123
|
clearPending(path)
|
|
106
124
|
syncTimers.set(path, setTimeout(() => {
|
|
107
125
|
syncTimers.delete(path)
|
|
108
|
-
rpc('edrv.lsp.sync', { sessionId, path, text, version:
|
|
126
|
+
rpc('edrv.lsp.sync', { sessionId, path, text, version: nextDocVer(path) }).catch(() => {})
|
|
109
127
|
}, SYNC_DEBOUNCE_MS))
|
|
110
128
|
}
|
|
111
129
|
|
|
@@ -117,6 +135,7 @@ function clearPending(path) {
|
|
|
117
135
|
/** 文档关闭:停止去抖并通知 host。 */
|
|
118
136
|
export function closeDoc(path) {
|
|
119
137
|
clearPending(path)
|
|
138
|
+
docVersions.delete(path)
|
|
120
139
|
void rpc('edrv.lsp.close', { sessionId, path }).catch(() => {})
|
|
121
140
|
}
|
|
122
141
|
|
|
@@ -171,22 +190,65 @@ export async function fetchHover(path, text, position) {
|
|
|
171
190
|
return res.hover || null
|
|
172
191
|
}
|
|
173
192
|
|
|
193
|
+
/**
|
|
194
|
+
* 成员触发字符:这类位置的补全最易被服务器的旧解析树错答(外层作用域 / 全局集)。
|
|
195
|
+
* @author ddj 2026年09月24号
|
|
196
|
+
*/
|
|
197
|
+
const MEMBER_TRIGGERS = new Set(['.', ':'])
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 记录成员补全诊断(仅日志,不改结果)。
|
|
201
|
+
*
|
|
202
|
+
* 现场:`ECommon.` 这类成员位置偶尔返回 `this` 的外层作用域成员或全局集
|
|
203
|
+
* (含 `this`/`...` 与大量 `CS_*`),根因指向服务器对刚输入文本的解析新鲜度,
|
|
204
|
+
* 且非确定性。此处只把每次成员补全的形态打标,供真实使用中取证后再定修复。
|
|
205
|
+
* @author ddj 2026年09月24号
|
|
206
|
+
* @param path 工作区相对路径
|
|
207
|
+
* @param position Monaco 位置(1-based)
|
|
208
|
+
* @param context LSP CompletionContext(取触发字符)
|
|
209
|
+
* @param prefix 当前输入前缀
|
|
210
|
+
* @param list 服务器归一化后的补全结果
|
|
211
|
+
*/
|
|
212
|
+
function logMemberDiag(path, position, context, prefix, list) {
|
|
213
|
+
const ch = context?.triggerCharacter
|
|
214
|
+
if (!MEMBER_TRIGGERS.has(ch)) return
|
|
215
|
+
const info = classifyMemberSet(list.items)
|
|
216
|
+
dbg(sessionId, '成员补全诊断 ch=' + ch
|
|
217
|
+
+ ' path=' + path
|
|
218
|
+
+ ' pos=' + position.lineNumber + ':' + position.column
|
|
219
|
+
+ ' prefix=' + JSON.stringify(prefix ?? '')
|
|
220
|
+
+ ' 条数=' + info.total
|
|
221
|
+
+ ' CS_*=' + info.csCount
|
|
222
|
+
+ ' 分类=' + info.kind
|
|
223
|
+
+ ' 首' + info.head.length + '=' + info.head.join(','))
|
|
224
|
+
}
|
|
225
|
+
|
|
174
226
|
/**
|
|
175
227
|
* 查询补全列表(列表阶段不带 documentation,由 resolve 补全)。
|
|
176
|
-
*
|
|
228
|
+
*
|
|
229
|
+
* `prefix` 必须随请求上报:host 在 500 条上限处做前缀感知截断,不带前缀时
|
|
230
|
+
* 只能按服务器原始顺序切,项目全局(ECommon 等)会被上万个 CS_* 桩挤出上限。
|
|
231
|
+
* @author ddj 2026年09月22号 / 2026年09月24号
|
|
177
232
|
* @param path 工作区相对路径
|
|
178
233
|
* @param text 文档全文(未保存内容必须进服务器,否则按旧文本解析)
|
|
179
234
|
* @param position Monaco 位置(1-based)
|
|
180
235
|
* @param context LSP CompletionContext(触发类型/触发字符)
|
|
236
|
+
* @param prefix 当前输入前缀(Monaco getWordUntilPosition 的词;可缺省)
|
|
181
237
|
* @returns { items, incomplete };失败或空时 null
|
|
182
238
|
*/
|
|
183
|
-
export async function fetchCompletions(path, text, position, context) {
|
|
239
|
+
export async function fetchCompletions(path, text, position, context, prefix) {
|
|
184
240
|
await syncDoc(path, text, true)
|
|
185
241
|
const args = { path, position: monoToLsp(position.lineNumber, position.column) }
|
|
186
242
|
if (context) args.context = context
|
|
243
|
+
if (prefix) args.prefix = prefix
|
|
187
244
|
const res = await queryLsp('查询补全', 'edrv.lsp.completion', args)
|
|
188
245
|
if (!res || !res.completions) return null
|
|
189
|
-
|
|
246
|
+
const list = res.completions
|
|
247
|
+
if (list.truncated === true) {
|
|
248
|
+
dbg(sessionId, '补全结果被截断(候选超上限):' + path + ' prefix=' + String(prefix ?? ''))
|
|
249
|
+
}
|
|
250
|
+
logMemberDiag(path, position, context, prefix, list)
|
|
251
|
+
return list
|
|
190
252
|
}
|
|
191
253
|
|
|
192
254
|
/**
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 成员补全结果集的形态分类(纯函数,node 可测)。
|
|
3
|
+
*
|
|
4
|
+
* 背景:`ECommon.` 这类成员位置偶尔被错答为**外层作用域集**(`this` 的成员)
|
|
5
|
+
* 或**全局集**(含 `this`/`...` 与大量 `CS_*` xLua 绑定桩),而正确答案应是
|
|
6
|
+
* `ECommon` 自身的字段。根因指向服务器对「刚输入文本」的解析新鲜度,且非确定性。
|
|
7
|
+
* 本模块只做「形态打标」供诊断日志使用,**不改变任何补全结果**。
|
|
8
|
+
*
|
|
9
|
+
* 作者 ddj 2026-09-24
|
|
10
|
+
*/
|
|
11
|
+
import type { LspCompletionItem } from '../../../shared/lsp.js'
|
|
12
|
+
|
|
13
|
+
/** 结果集形态:empty 空;globals 全局集;outerScope 外层作用域集;member 成员集;other 其他。 */
|
|
14
|
+
export type CompletionSetKind = 'empty' | 'globals' | 'outerScope' | 'member' | 'other'
|
|
15
|
+
|
|
16
|
+
/** 分类结果:形态 + 计数 + 前几项(供日志与人工判读)。 */
|
|
17
|
+
export interface CompletionSetInfo {
|
|
18
|
+
/** 形态标签 */
|
|
19
|
+
kind: CompletionSetKind
|
|
20
|
+
/** 条目总数 */
|
|
21
|
+
total: number
|
|
22
|
+
/** `CS_*` 绑定桩条数 */
|
|
23
|
+
csCount: number
|
|
24
|
+
/** 绑定桩占比(0–1;空集为 0) */
|
|
25
|
+
csRatio: number
|
|
26
|
+
/** 前若干条 label(便于人工判读,不参与判定) */
|
|
27
|
+
head: string[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 外层作用域(`this` 成员)特征名。
|
|
32
|
+
*
|
|
33
|
+
* ⚠️ 这是**捕获派生的启发式标记**,不是通用的作用域判定:名单来自 2026-09-24
|
|
34
|
+
* 在 `RechargeHeartView.lua` 真实键盘复现到的 87 条错答样本(`this` 成员列表)。
|
|
35
|
+
* 用途仅是把「疑似外层作用域」的观测标出来供取证;未命中足够标记时归入 `member`,
|
|
36
|
+
* 因此**不会**把正常的成员集误报成外层集。
|
|
37
|
+
*/
|
|
38
|
+
const INSTANCE_MARKERS = new Set([
|
|
39
|
+
'addframe', 'addtimer', 'clearalltimers', 'buyheart', 'canvas', 'canvasgroup',
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 取条目匹配文本(Monaco 口径:filterText 优先,回落 label)。
|
|
44
|
+
*
|
|
45
|
+
* 与 host 侧 `lsp/completionCap.ts` 的同名私有函数刻意各留一份:两者分属
|
|
46
|
+
* host / client 两个打包平面,不为 3 行逻辑建立跨平面依赖。
|
|
47
|
+
* @author ddj 2026年09月24号
|
|
48
|
+
* @param item 补全项
|
|
49
|
+
* @returns 匹配文本;无可用文本时为空串
|
|
50
|
+
*/
|
|
51
|
+
function textOfItem(item: LspCompletionItem): string {
|
|
52
|
+
const raw = typeof item?.filterText === 'string' && item.filterText ? item.filterText : item?.label
|
|
53
|
+
return typeof raw === 'string' ? raw : ''
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 依据条目形态判定结果集属于哪一类。
|
|
58
|
+
* @author ddj 2026年09月24号
|
|
59
|
+
* @param lower 全部条目文本(已小写)
|
|
60
|
+
* @param total 条目总数
|
|
61
|
+
* @param csCount `CS_*` 条数
|
|
62
|
+
* @param csRatio `CS_*` 占比
|
|
63
|
+
* @returns 形态标签
|
|
64
|
+
*/
|
|
65
|
+
function kindOf(lower: string[], total: number, csCount: number, csRatio: number): CompletionSetKind {
|
|
66
|
+
if (total === 0) return 'empty'
|
|
67
|
+
if (lower.includes('this') || lower.includes('...') || csRatio > 0.5) return 'globals'
|
|
68
|
+
if (csCount === 0 && lower.filter((t) => INSTANCE_MARKERS.has(t)).length >= 2) return 'outerScope'
|
|
69
|
+
if (csCount === 0) return 'member'
|
|
70
|
+
return 'other'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 分类成员补全结果集(纯函数,无副作用)。
|
|
75
|
+
* @author ddj 2026年09月24号
|
|
76
|
+
* @param items 服务器归一化后的补全条目(非数组按空处理)
|
|
77
|
+
* @param headSize 日志携带的前缀条数(缺省 3)
|
|
78
|
+
* @returns 形态、计数与前几项
|
|
79
|
+
*/
|
|
80
|
+
export function classifyMemberSet(items: LspCompletionItem[], headSize: number = 3): CompletionSetInfo {
|
|
81
|
+
const list = Array.isArray(items) ? items : []
|
|
82
|
+
// 判定用 filterText 优先(与过滤器同口径);日志展示用 label(人工判读友好)。
|
|
83
|
+
const texts = list.map(textOfItem)
|
|
84
|
+
const lower = texts.map((t) => t.toLowerCase())
|
|
85
|
+
const total = list.length
|
|
86
|
+
const csCount = texts.filter((t) => t.startsWith('CS_')).length
|
|
87
|
+
const csRatio = total ? csCount / total : 0
|
|
88
|
+
const head = list.slice(0, Math.max(0, headSize)).map((item) => String(item?.label ?? ''))
|
|
89
|
+
return { kind: kindOf(lower, total, csCount, csRatio), total, csCount, csRatio, head }
|
|
90
|
+
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
LSP_SEMANTIC_TOKEN_MODIFIERS, LSP_SEMANTIC_TOKEN_TYPES,
|
|
19
19
|
LSP_COMPLETION_KIND_NAMES, LSP_INSERT_TEXT_FORMAT_SNIPPET,
|
|
20
20
|
} from '../../../shared/lsp.js'
|
|
21
|
+
import { luaKeywordsAt, luaTypesAt, mergeKeywords } from './keywords.js'
|
|
21
22
|
|
|
22
23
|
const LSP_LANGS = ['lua', 'csharp']
|
|
23
24
|
|
|
@@ -352,18 +353,27 @@ function completionsFor(monaco, lang) {
|
|
|
352
353
|
provideCompletionItems: async (model, position, context, token) => {
|
|
353
354
|
const path = pathOfModel(model)
|
|
354
355
|
if (!path) return { suggestions: [] }
|
|
355
|
-
const list = await fetchCompletions(path, model.getValue(), position, toLspContext(context))
|
|
356
|
-
if (!list || !list.items.length || token?.isCancellationRequested) return { suggestions: [] }
|
|
357
356
|
const word = model.getWordUntilPosition(position)
|
|
357
|
+
// 前缀必须上报:host 在条目上限处按前缀分桶截断,否则项目全局
|
|
358
|
+
// (ECommon 等)会被 xLua 绑定桩(上万个 CS_*)挤出上限且无法恢复。
|
|
359
|
+
const list = await fetchCompletions(path, model.getValue(), position, toLspContext(context), word.word)
|
|
360
|
+
if (token?.isCancellationRequested) return { suggestions: [] }
|
|
361
|
+
if (lang !== 'lua' && (!list || !list.items.length)) return { suggestions: [] }
|
|
358
362
|
const fallback = {
|
|
359
363
|
startLineNumber: position.lineNumber,
|
|
360
364
|
endLineNumber: position.lineNumber,
|
|
361
365
|
startColumn: word.startColumn,
|
|
362
366
|
endColumn: word.endColumn,
|
|
363
367
|
}
|
|
368
|
+
const serverItems = (list?.items ?? []).map((item) => toMonoSuggestion(monaco, item, fallback, path))
|
|
364
369
|
return {
|
|
365
|
-
suggestions:
|
|
366
|
-
|
|
370
|
+
suggestions: lang === 'lua'
|
|
371
|
+
? mergeKeywords(serverItems, [
|
|
372
|
+
...luaKeywordsAt(monaco, model, position, fallback),
|
|
373
|
+
...luaTypesAt(monaco, model, position, fallback),
|
|
374
|
+
])
|
|
375
|
+
: serverItems,
|
|
376
|
+
incomplete: list?.incomplete === true,
|
|
367
377
|
}
|
|
368
378
|
},
|
|
369
379
|
resolveCompletionItem: async (item, token) => {
|