dsh-vscode-mode 0.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.10.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": [
@@ -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) && !LUA_KEYWORDS.has(text)
49
+ return EVAL_EXPR.test(text) && !LUA_RESERVED_WORD_SET.has(text)
55
50
  }
56
51
 
57
52
  /**
@@ -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: 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
+ }
@@ -504,6 +504,7 @@ function treeRunCtx(target: TreeMenuTarget, ctx: SidebarCtx): SvnActionRunCtx {
504
504
  path: target.path,
505
505
  notify: (message) => ctx.notify?.(message),
506
506
  openSvnDiff: (p) => ctx.openSvnDiff?.(p),
507
+ openSvnUpdate: (p) => ctx.openSvnUpdate?.(p),
507
508
  openSvnLog: ctx.openSvnLog,
508
509
  refreshChanges: ctx.refreshSvnChanges,
509
510
  // 确认对话框可注入(测试/宿主可替换;缺省走 window.confirm)
@@ -45,6 +45,8 @@ export interface SidebarCtx {
45
45
  openSvnLocalPair?: (left: string, right: string, leftLabel: string, rightLabel: string, title: string) => void
46
46
  /** 重查 SVN 变更清单(CLI 更新/还原/加入版本控制后由菜单动作调用)。 */
47
47
  refreshSvnChanges?: () => void
48
+ /** 打开 SVN 更新窗口(树菜单与其它入口共用)。 */
49
+ openSvnUpdate?: (path: string) => void
48
50
  /** 打开 SVN 日志弹窗(P3;菜单/命令栏「查看日志」)。 */
49
51
  openSvnLog?: (path?: string) => void
50
52
  /** 确认对话框(破坏性动作守卫;缺省走 window.confirm,测试可注入)。 */