dsh-vscode-mode 0.10.0 → 0.12.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 +62 -11
- package/lib/client.js +2879 -1030
- package/lib/client.js.map +1 -1
- package/lib/index.js +2227 -1056
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/activePathStore.ts +28 -0
- package/src/client/commandBridge.ts +9 -41
- package/src/client/dap/hover.ts +2 -7
- package/src/client/index.ts +116 -14
- package/src/client/keybindings.ts +19 -73
- 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/shortcutsOfficial.ts +451 -0
- package/src/client/sidebar/menuItems.ts +1 -0
- package/src/client/sidebar/panels/RulesPanel.ts +15 -11
- 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 +142 -135
- package/src/client/ui/McpSettings.ts +4 -5
- package/src/client/ui/QuickOpen.ts +10 -11
- package/src/client/ui/ShortcutSettings.ts +294 -0
- package/src/client/ui/SvnLogDialog.ts +200 -69
- package/src/client/ui/SvnUpdateDialog.ts +472 -0
- package/src/client/ui/commandCatalog.ts +36 -41
- package/src/client/ui/svnUpdateModel.ts +135 -0
- package/src/fileOpenSettings.ts +3 -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/rules.ts +100 -44
- package/src/settingsDocPath.ts +30 -0
- package/src/shared/keybindings.ts +253 -0
- package/src/shared/rpc.ts +12 -2
- package/src/shared/rules.ts +9 -1
- 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/src/client/ui/KeybindingsPanel.ts +0 -190
|
@@ -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) => {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Redirect the DSH settings document action into the plugin editor. */
|
|
2
|
+
import { patchAccessor } from './compat.js'
|
|
3
|
+
|
|
4
|
+
export interface SettingsDocResult {
|
|
5
|
+
ok: boolean
|
|
6
|
+
value?: { opened: boolean }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type SettingsDocOpen = (...args: unknown[]) => Promise<SettingsDocResult>
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Patch the optional DSH settings document opener; failed handoff keeps native behavior.
|
|
13
|
+
* @author ddj 2026年10月02号
|
|
14
|
+
* @param service DSH remote settings namespace
|
|
15
|
+
* @param getPath plugin RPC that prepares the profile document
|
|
16
|
+
* @param openEditor current plugin editor action
|
|
17
|
+
* @param logger optional diagnostic sink
|
|
18
|
+
* @returns disposer, or null if the DSH API is unavailable
|
|
19
|
+
*/
|
|
20
|
+
export function patchSettingsDoc(
|
|
21
|
+
service: object,
|
|
22
|
+
getPath: () => Promise<{ ok: boolean; path?: string; error?: string }>,
|
|
23
|
+
openEditor: (path: string) => void,
|
|
24
|
+
logger?: (message: string) => void,
|
|
25
|
+
): (() => void) | null {
|
|
26
|
+
return patchAccessor(service, 'openSettingsDocument', async (original, ...args) => {
|
|
27
|
+
try {
|
|
28
|
+
const result = await getPath()
|
|
29
|
+
if (!result.ok || !result.path) throw new Error(result.error || '设置配置文件路径不可用')
|
|
30
|
+
openEditor(result.path)
|
|
31
|
+
return { ok: true, value: { opened: true } }
|
|
32
|
+
} catch (error) {
|
|
33
|
+
logger?.('插件编辑器打开设置配置文件失败,回退 DSH 原生打开: ' + String(error))
|
|
34
|
+
return (original as SettingsDocOpen)(...args)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Safely locate the optional DSH settings remote service.
|
|
41
|
+
* @author ddj 2026年10月02号
|
|
42
|
+
* @param ctx Client service context
|
|
43
|
+
* @returns The settings service when its document opener exists
|
|
44
|
+
*/
|
|
45
|
+
export function probeSettingsDoc(ctx: { get: (name: string) => unknown }): object | undefined {
|
|
46
|
+
try {
|
|
47
|
+
const service = ctx.get('remote.settings') as { openSettingsDocument?: unknown } | undefined
|
|
48
|
+
return typeof service?.openSettingsDocument === 'function' ? service : undefined
|
|
49
|
+
} catch {
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
}
|