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.
Files changed (49) hide show
  1. package/README.md +62 -11
  2. package/lib/client.js +2879 -1030
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +2227 -1056
  5. package/lib/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/client/activePathStore.ts +28 -0
  8. package/src/client/commandBridge.ts +9 -41
  9. package/src/client/dap/hover.ts +2 -7
  10. package/src/client/index.ts +116 -14
  11. package/src/client/keybindings.ts +19 -73
  12. package/src/client/luaReservedWords.ts +10 -0
  13. package/src/client/monaco/lsp/keywords.ts +121 -0
  14. package/src/client/monaco/lsp/lspClient.ts +68 -6
  15. package/src/client/monaco/lsp/memberScope.ts +90 -0
  16. package/src/client/monaco/lsp/providers.ts +14 -4
  17. package/src/client/settingsDocRouter.ts +52 -0
  18. package/src/client/shortcutsOfficial.ts +451 -0
  19. package/src/client/sidebar/menuItems.ts +1 -0
  20. package/src/client/sidebar/panels/RulesPanel.ts +15 -11
  21. package/src/client/sidebar/types.ts +2 -0
  22. package/src/client/styles/editor.css +143 -37
  23. package/src/client/svnActions.ts +4 -4
  24. package/src/client/ui/ContextMenu.ts +4 -2
  25. package/src/client/ui/EditorView.ts +142 -135
  26. package/src/client/ui/McpSettings.ts +4 -5
  27. package/src/client/ui/QuickOpen.ts +10 -11
  28. package/src/client/ui/ShortcutSettings.ts +294 -0
  29. package/src/client/ui/SvnLogDialog.ts +200 -69
  30. package/src/client/ui/SvnUpdateDialog.ts +472 -0
  31. package/src/client/ui/commandCatalog.ts +36 -41
  32. package/src/client/ui/svnUpdateModel.ts +135 -0
  33. package/src/fileOpenSettings.ts +3 -0
  34. package/src/index.ts +2 -0
  35. package/src/lsp/completionCap.ts +80 -0
  36. package/src/lsp/manager.ts +42 -10
  37. package/src/lsp/rpc.ts +8 -5
  38. package/src/lsp/server.ts +18 -4
  39. package/src/rpc.ts +74 -2
  40. package/src/rules.ts +100 -44
  41. package/src/settingsDocPath.ts +30 -0
  42. package/src/shared/keybindings.ts +253 -0
  43. package/src/shared/rpc.ts +12 -2
  44. package/src/shared/rules.ts +9 -1
  45. package/src/shared/svn.ts +38 -0
  46. package/src/svn.ts +102 -7
  47. package/src/svnText.ts +22 -13
  48. package/src/svnUpdateJob.ts +588 -0
  49. 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: Date.now() }).catch(() => {})
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: Date.now() }).catch(() => {})
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
- * @author ddj 2026年09月22号
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
- return res.completions
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: list.items.map((item) => toMonoSuggestion(monaco, item, fallback, path)),
366
- incomplete: list.incomplete === true,
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
+ }