dsh-vscode-mode 0.4.6 → 0.5.1

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 (55) hide show
  1. package/README.md +38 -2
  2. package/lib/client.js +9316 -4038
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +1852 -68
  5. package/lib/index.js.map +1 -1
  6. package/package.json +8 -8
  7. package/src/client/ai/inlineProvider.ts +42 -1
  8. package/src/client/index.ts +50 -45
  9. package/src/client/monaco/lsp/index.ts +33 -3
  10. package/src/client/monaco/lsp/lspClient.ts +3 -1
  11. package/src/client/monaco/lsp/providers.ts +60 -6
  12. package/src/client/navHighlight.ts +96 -0
  13. package/src/client/officialSidebar.ts +67 -3
  14. package/src/client/paths.ts +2 -0
  15. package/src/client/rpc.ts +32 -12
  16. package/src/client/sessionScope.ts +183 -0
  17. package/src/client/sidebar/SidebarView.ts +6 -2
  18. package/src/client/sidebar/menuItems.ts +93 -2
  19. package/src/client/sidebar/panels/FileExplorer.ts +18 -1
  20. package/src/client/sidebar/panels/SvnPanel.ts +303 -0
  21. package/src/client/sidebar/panels/index.ts +44 -3
  22. package/src/client/sidebar/types.ts +20 -0
  23. package/src/client/snippets/provider.ts +26 -4
  24. package/src/client/styles/editor.css +177 -0
  25. package/src/client/svnActions.ts +153 -0
  26. package/src/client/svnLog.ts +356 -0
  27. package/src/client/svnStatus.ts +363 -0
  28. package/src/client/svnStore.ts +150 -0
  29. package/src/client/tabActions.ts +30 -5
  30. package/src/client/tabMenu.ts +51 -0
  31. package/src/client/ui/AiSettings.ts +8 -0
  32. package/src/client/ui/EditorView.ts +632 -35
  33. package/src/client/ui/LogDialog.ts +333 -0
  34. package/src/client/ui/McpSettings.ts +98 -0
  35. package/src/client/ui/ModalShell.ts +79 -0
  36. package/src/client/ui/SideBySideDiff.tsx +139 -0
  37. package/src/client/ui/SnippetsPicker.ts +70 -58
  38. package/src/client/ui/SvnDiffPanel.ts +50 -0
  39. package/src/client/ui/SvnLogDialog.ts +1141 -0
  40. package/src/client/ui/SvnLogStats.ts +243 -0
  41. package/src/client/ui/commandCatalog.ts +72 -0
  42. package/src/compat.ts +1 -1
  43. package/src/debugLog.ts +81 -12
  44. package/src/dshVersion.ts +8 -1
  45. package/src/fileOpenSettings.ts +40 -3
  46. package/src/index.ts +5 -1
  47. package/src/rpc.ts +41 -4
  48. package/src/shared/ai.ts +25 -0
  49. package/src/shared/rpc.ts +53 -1
  50. package/src/shared/svn.ts +377 -0
  51. package/src/shared/svnActions.ts +296 -0
  52. package/src/svn.ts +870 -0
  53. package/src/svnLog.ts +315 -0
  54. package/src/svnText.ts +175 -0
  55. package/src/svnXml.ts +110 -0
@@ -0,0 +1,303 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — 侧边栏「SVN 变更」面板(工作副本状态视图)。
4
+ *
5
+ * 形态对齐 TortoiseSVN「Check for Modifications」的**列表 + 分组 + 行内动作**骨架:
6
+ * 头部(标题 / 工作副本根名 / ⟳)+ 显示开关(未版本控制 / 忽略项)+ 批量条 +
7
+ * 按 changelist 分组的行列表 + 空态/加载态/未受管理态。行 = 状态字母(按 SVN_STATUS_TONE
8
+ * 着色)+ 相对路径 + 动作按钮(比较 / 加入 / 还原);双击行走「与基线比较」。
9
+ *
10
+ * 数据来源:ctx.svnChanges(EditorView 经 svnStatus.ensureSvnChanges 拉取后注入),
11
+ * 本面板不自行发 RPC,避免与文件树重复拉取;⟳ 触发 refreshSvnChanges 后由事件回流重渲染。
12
+ * 显示开关持久化到 localStorage(CACHE_KEY.svn + scope),与既有面板一致。
13
+ * 作者 ddj 2026年09月16号
14
+ */
15
+ import React from 'react'
16
+ import { IconRefreshOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
17
+ import {
18
+ SVN_ADD_LABEL,
19
+ SVN_DIFF_BASE_LABEL,
20
+ SVN_REVERT_LABEL,
21
+ SVN_STATUS_LABEL,
22
+ SVN_STATUS_LETTER,
23
+ SVN_STATUS_TONE,
24
+ isSvnDiffable,
25
+ svnVisibleChanges,
26
+ } from '../../../shared/svn.js'
27
+ import { CACHE_KEY } from '../../paths.js'
28
+ import { refreshSvnChanges, svnAdd, svnRevert } from '../../svnStatus.js'
29
+ import type { SidebarCtx } from '../types.js'
30
+
31
+ /** 未分组条目的分组键(changelist 为空)。 */
32
+ const NO_CHANGELIST = '(未分组)'
33
+
34
+ /** 还原确认文案(新增文件不会被删除是 svn revert 的实际语义,须如实告知)。 */
35
+ const REVERT_CONFIRM_HEAD = '确认还原以下 '
36
+ const REVERT_CONFIRM_TAIL = ' 个文件的本地改动?新增(A)文件在磁盘上会保留,仅取消登记。'
37
+
38
+ // --region 显示开关持久化
39
+
40
+ /**
41
+ * 读取面板显示开关(损坏/不可用安全)。
42
+ * @author ddj 2026年09月16号
43
+ * @param scope 作用域键
44
+ * @returns 开关(默认:显示未版本控制、隐藏忽略项)
45
+ */
46
+ function loadFilter(scope) {
47
+ const fallback = { unversioned: true, ignored: false }
48
+ if (!scope) return fallback
49
+ try {
50
+ const raw = window.localStorage.getItem(CACHE_KEY.svn + scope)
51
+ if (!raw) return fallback
52
+ const parsed = JSON.parse(raw)
53
+ return {
54
+ unversioned: parsed?.unversioned !== false,
55
+ ignored: parsed?.ignored === true,
56
+ }
57
+ } catch (error) {
58
+ return fallback
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 写面板显示开关(配额/隐私模式失败静默)。
64
+ * @author ddj 2026年09月16号
65
+ * @param scope 作用域键
66
+ * @param filter 开关
67
+ */
68
+ function saveFilter(scope, filter) {
69
+ if (!scope) return
70
+ try {
71
+ window.localStorage.setItem(CACHE_KEY.svn + scope, JSON.stringify(filter))
72
+ } catch (error) { /* 配额满/隐私模式忽略 */ }
73
+ }
74
+ // --endregion
75
+
76
+ // --region 展示元素
77
+
78
+ /**
79
+ * 刷新按钮图标(官方 IconRefreshOutline16;原语缺失回落文本 ⟳)。
80
+ * @author ddj 2026年09月16号
81
+ * @returns 图标元素或回落文本
82
+ */
83
+ function refreshIconEl() {
84
+ if (typeof IconRefreshOutline16 !== 'function') return '⟳'
85
+ return React.createElement(IconRefreshOutline16, { size: 14 })
86
+ }
87
+
88
+ /**
89
+ * 条目状态字母元素(按 tone 着色)。
90
+ * @author ddj 2026年09月16号
91
+ * @param entry 变更条目
92
+ * @returns 字母元素
93
+ */
94
+ function statusLetterEl(entry) {
95
+ const tone = SVN_STATUS_TONE[entry.status] ?? 'plain'
96
+ return React.createElement('span', {
97
+ className: 'edrv-svn-ch edrv-svn-ch-' + tone,
98
+ title: SVN_STATUS_LABEL[entry.status] ?? entry.status,
99
+ }, SVN_STATUS_LETTER[entry.status] ?? '?')
100
+ }
101
+
102
+ /**
103
+ * 按 changelist 把条目分组(未分组恒排末位;组内保持工作副本原顺序)。
104
+ * @author ddj 2026年09月16号
105
+ * @param entries 已过滤的变更清单
106
+ * @returns 分组数组(name + entries)
107
+ */
108
+ export function groupByChangelist(entries) {
109
+ const groups = new Map()
110
+ for (const entry of entries) {
111
+ const name = entry.changelist || NO_CHANGELIST
112
+ const bucket = groups.get(name)
113
+ if (bucket) bucket.push(entry)
114
+ else groups.set(name, [entry])
115
+ }
116
+ const named = [...groups.entries()].filter(([name]) => name !== NO_CHANGELIST)
117
+ const rest = groups.get(NO_CHANGELIST)
118
+ if (rest) named.push([NO_CHANGELIST, rest])
119
+ return named.map(([name, items]) => ({ name, entries: items }))
120
+ }
121
+
122
+ /**
123
+ * 单条变更行(状态字母 + 路径 + 行内动作;双击 = 与基线比较)。
124
+ * @author ddj 2026年09月16号
125
+ * @param props.entry 变更条目
126
+ * @param props.busy 批量动作进行中(按钮置灰)
127
+ * @param props.onDiff 打开基线差异
128
+ * @param props.onAdd 加入版本控制
129
+ * @param props.onRevert 还原
130
+ * @returns 行元素
131
+ */
132
+ function SvnRow(props) {
133
+ const { entry, busy, onDiff, onAdd, onRevert } = props
134
+ const diffable = isSvnDiffable(entry.path, entry.status)
135
+ const act = (label, title, danger, run) => React.createElement('button', {
136
+ className: 'edrv-svn-act' + (danger ? ' edrv-svn-act-danger' : ''),
137
+ title,
138
+ disabled: busy,
139
+ onClick: (event) => { event.stopPropagation(); run() },
140
+ }, label)
141
+ return React.createElement('div', {
142
+ className: 'edrv-svn-row',
143
+ title: entry.path,
144
+ onDoubleClick: () => { if (diffable) onDiff(entry.path) },
145
+ },
146
+ statusLetterEl(entry),
147
+ React.createElement('span', { className: 'edrv-svn-path' }, entry.path),
148
+ React.createElement('span', { className: 'edrv-svn-acts' },
149
+ (diffable ? act('比较', SVN_DIFF_BASE_LABEL + '(BASE 与工作区并排)', false, () => onDiff(entry.path)) : null),
150
+ (!entry.versioned ? act('加入', SVN_ADD_LABEL, false, () => onAdd([entry.path])) : null),
151
+ (entry.versioned ? act('还原', SVN_REVERT_LABEL + '(放弃本地改动)', true, () => onRevert([entry.path])) : null)))
152
+ }
153
+
154
+ /**
155
+ * 列表主体:分组行 + 条目行;未受管理/加载中/无变更各自给出稳定文案。
156
+ * @author ddj 2026年09月16号
157
+ * @param state 列表状态(managed/entries/visible/groups)
158
+ * @param handlers 行动作(onDiff/onAdd/onRevert/busy)
159
+ * @returns 主体元素或元素数组
160
+ */
161
+ function svnListBody(state, handlers) {
162
+ if (!state.managed) return React.createElement('div', { className: 'edrv-tree-loading' }, '当前工作区不受 SVN 管理')
163
+ if (state.entries === null) return React.createElement('div', { className: 'edrv-tree-loading' }, '读取变更中…')
164
+ if (!state.visible.length) return React.createElement('div', { className: 'edrv-tree-loading' }, '无变更(工作副本干净)')
165
+ const rows = []
166
+ for (const group of state.groups) {
167
+ if (state.groups.length > 1 || group.name !== NO_CHANGELIST) {
168
+ rows.push(React.createElement('div', { key: 'g:' + group.name, className: 'edrv-svn-group' },
169
+ React.createElement('span', null, 'changelist: ' + group.name),
170
+ React.createElement('span', { className: 'edrv-svn-group-n' }, String(group.entries.length))))
171
+ }
172
+ for (const entry of group.entries) {
173
+ rows.push(React.createElement(SvnRow, Object.assign({ key: entry.path, entry }, handlers)))
174
+ }
175
+ }
176
+ return rows
177
+ }
178
+
179
+ /**
180
+ * 工具条(显示开关 + 批量动作)。
181
+ * @author ddj 2026年09月16号
182
+ * @param props.filter 当前开关
183
+ * @param props.count 可见条目数
184
+ * @param props.paths 批量动作目标(add/revert 各自清单)
185
+ * @param props.handlers 动作回调(toggle/add/revert/busy)
186
+ * @returns 工具条元素数组
187
+ */
188
+ function svnToolbarEl(props) {
189
+ const { filter, count, paths, handlers } = props
190
+ const check = (label, title, checked, onChange) => React.createElement('label', {
191
+ className: 'edrv-svn-check', title,
192
+ },
193
+ React.createElement('input', { type: 'checkbox', checked, onChange }),
194
+ React.createElement('span', null, label))
195
+ return [
196
+ React.createElement('div', { key: 'tools', className: 'edrv-svn-toolbar' },
197
+ check('未版本控制', '显示未纳入版本控制的文件', filter.unversioned, handlers.toggleUnversioned),
198
+ check('忽略项', '显示被忽略的文件', filter.ignored, handlers.toggleIgnored)),
199
+ (count
200
+ ? React.createElement('div', { key: 'bulk', className: 'edrv-svn-bulk' },
201
+ React.createElement('span', { className: 'edrv-svn-count' }, String(count) + ' 项'),
202
+ React.createElement('span', { style: { flex: 1 } }),
203
+ (paths.unversioned.length
204
+ ? React.createElement('button', {
205
+ className: 'edrv-svn-act', disabled: handlers.busy,
206
+ title: '把全部未版本控制文件加入版本控制',
207
+ onClick: () => handlers.add(paths.unversioned),
208
+ }, '全部加入')
209
+ : null),
210
+ (paths.revert.length
211
+ ? React.createElement('button', {
212
+ className: 'edrv-svn-act edrv-svn-act-danger', disabled: handlers.busy,
213
+ title: '还原全部已受版本控制文件的本地改动',
214
+ onClick: () => handlers.revert(paths.revert),
215
+ }, '全部还原')
216
+ : null))
217
+ : null),
218
+ ]
219
+ }
220
+ // --endregion
221
+
222
+ /**
223
+ * 「SVN 变更」面板主体。
224
+ * @author ddj 2026年09月16号
225
+ * @param props.ctx 面板共享上下文(sessionId/scope/svnChanges/svn/notify/openSvnDiff)
226
+ * @returns 面板元素
227
+ */
228
+ export function SvnPanel(props) {
229
+ const ctx = props?.ctx
230
+ const sessionId = ctx?.sessionId
231
+ const scope = ctx?.scope
232
+ const entries = ctx?.svnChanges ?? null
233
+ const managed = Boolean(ctx?.svn?.managed && ctx?.svn?.svnCli)
234
+ const [filter, setFilter] = React.useState(() => loadFilter(scope))
235
+ const [busy, setBusy] = React.useState(false)
236
+
237
+ // 作用域切换:重读该作用域自己的开关(同一工作区共享)
238
+ React.useEffect(() => {
239
+ setFilter(loadFilter(scope))
240
+ }, [scope])
241
+
242
+ const applyFilter = (next) => {
243
+ setFilter(next)
244
+ saveFilter(scope, next)
245
+ }
246
+
247
+ /**
248
+ * 批量动作统一流程:先 confirm(仅还原需要)→ 执行 → 反馈 → 强制重查。
249
+ * @author ddj 2026年09月16号
250
+ * @param kind 'add' | 'revert'
251
+ * @param paths 目标相对路径列表
252
+ */
253
+ const runBatch = (kind, paths) => {
254
+ if (!paths.length) return
255
+ if (kind === 'revert') {
256
+ const head = REVERT_CONFIRM_HEAD + paths.length + REVERT_CONFIRM_TAIL
257
+ if (!window.confirm(head + '\n\n' + paths.join('\n'))) return
258
+ }
259
+ setBusy(true)
260
+ const task = kind === 'revert' ? svnRevert(sessionId, paths) : svnAdd(sessionId, paths)
261
+ void task.then((outcome) => {
262
+ ctx?.notify?.(outcome.message)
263
+ refreshSvnChanges(sessionId, scope)
264
+ }).finally(() => setBusy(false))
265
+ }
266
+
267
+ const visible = entries === null ? [] : svnVisibleChanges(entries, filter)
268
+ const openSvnDiff = (p) => ctx?.openSvnDiff?.(p)
269
+ const handlers = {
270
+ busy,
271
+ onDiff: openSvnDiff,
272
+ onAdd: (paths) => runBatch('add', paths),
273
+ onRevert: (paths) => runBatch('revert', paths),
274
+ }
275
+ const listState = { managed, entries, visible, groups: groupByChangelist(visible) }
276
+ const toolbar = svnToolbarEl({
277
+ filter,
278
+ count: visible.length,
279
+ paths: {
280
+ unversioned: visible.filter((entry) => !entry.versioned).map((entry) => entry.path),
281
+ revert: visible.filter((entry) => entry.versioned).map((entry) => entry.path),
282
+ },
283
+ handlers: Object.assign({}, handlers, {
284
+ toggleUnversioned: () => applyFilter({ ...filter, unversioned: !filter.unversioned }),
285
+ toggleIgnored: () => applyFilter({ ...filter, ignored: !filter.ignored }),
286
+ add: (paths) => runBatch('add', paths),
287
+ revert: (paths) => runBatch('revert', paths),
288
+ }),
289
+ })
290
+ const rootName = ctx?.svn?.wcRoot ? String(ctx.svn.wcRoot).split(/[\\/]/).pop() || ctx.svn.wcRoot : ''
291
+
292
+ return React.createElement('div', { className: 'edrv-side-panel' },
293
+ React.createElement('div', { className: 'edrv-side-head' },
294
+ React.createElement('span', { className: 'edrv-side-title' }, 'SVN 变更'),
295
+ React.createElement('span', { className: 'edrv-side-root', title: ctx?.svn?.wcRoot || '' }, rootName),
296
+ React.createElement('span', { style: { flex: 1 } }),
297
+ React.createElement('button', {
298
+ className: 'edrv-side-btn', title: '刷新变更',
299
+ onClick: () => { refreshSvnChanges(sessionId, scope); ctx?.refreshRecords?.() },
300
+ }, refreshIconEl())),
301
+ ...toolbar,
302
+ React.createElement('div', { className: 'edrv-svn-list' }, svnListBody(listState, handlers)))
303
+ }
@@ -1,14 +1,16 @@
1
1
  // @ts-nocheck
2
2
  /**
3
- * dsh-vscode-mode client — 侧边栏面板定义(文件管理 + 搜索 + 规则)。
3
+ * dsh-vscode-mode client — 侧边栏面板定义(文件管理 + 搜索 + 规则 + SVN 变更)。
4
4
  * 活动栏图标用官方 UI 原语(跟随 DSH 主题与皮肤);原语缺失时由 SidebarView 回落文本渲染。
5
- * 作者 ddj 2026-08-26 / 2026-09-03 / 2026-09-10
5
+ * 「SVN 变更」带 visible 守卫:仅受 SVN 管理的工作区出现(非 SVN 工作区零变化)。
6
+ * 作者 ddj 2026-08-26 / 2026-09-03 / 2026-09-10 / 2026-09-16
6
7
  */
7
8
  import React from 'react'
8
- import { IconFolderOpenOutline16, IconListPenOutline16, IconSearchOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
9
+ import { IconBranchOutline16, IconFolderOpenOutline16, IconListPenOutline16, IconSearchOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
9
10
  import { FileExplorer } from './FileExplorer.js'
10
11
  import { SearchPanel } from './SearchPanel.js'
11
12
  import { RulesPanel } from './RulesPanel.js'
13
+ import { SvnPanel } from './SvnPanel.js'
12
14
  import type { SidebarPanelDef, SidebarCtx } from '../types.js'
13
15
 
14
16
  /**
@@ -56,3 +58,42 @@ export function createRulesPanel(): SidebarPanelDef {
56
58
  render: (ctx: SidebarCtx) => React.createElement(RulesPanel, { ctx }),
57
59
  }
58
60
  }
61
+
62
+ /**
63
+ * 构造「SVN 变更」面板定义。
64
+ * 徽标 = 过滤后仍显示的变更数(0/未加载不显示);visible 守卫保证非 SVN 工作区
65
+ * 既无活动栏图标也无面板(避免出现永远空的入口)。
66
+ * @author ddj 2026年09月16号
67
+ * @returns 面板定义(图标 = 官方 IconBranchOutline16;无原语时回落文本)
68
+ */
69
+ export function createSvnPanel(): SidebarPanelDef {
70
+ return {
71
+ id: 'svn',
72
+ title: 'SVN 变更',
73
+ icon: typeof IconBranchOutline16 === 'function' ? IconBranchOutline16 : '⎇',
74
+ order: 25,
75
+ visible: (ctx: SidebarCtx) => Boolean(ctx?.svn?.managed && ctx?.svn?.svnCli),
76
+ badge: (ctx: SidebarCtx) => {
77
+ const entries = ctx?.svnChanges
78
+ if (!Array.isArray(entries)) return null
79
+ const count = svnVisibleCount(entries)
80
+ return count > 0 ? count : null
81
+ },
82
+ render: (ctx: SidebarCtx) => React.createElement(SvnPanel, { ctx }),
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 徽标计数:与面板默认过滤口径一致(显示未版本控制、隐藏忽略项)。
88
+ * @author ddj 2026年09月16号
89
+ * @param entries 变更清单
90
+ * @returns 计数
91
+ */
92
+ function svnVisibleCount(entries) {
93
+ let count = 0
94
+ for (const entry of entries) {
95
+ if (entry?.status === 'ignored' || entry?.status === 'normal' || entry?.status === 'external') continue
96
+ count += 1
97
+ }
98
+ return count
99
+ }
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import type { OutlineSourceRegistry } from '../outline/types.js'
7
7
  import type { AddToConversation } from '../addToConversation.js'
8
+ import type { SvnChangeEntry, SvnStatusPayload } from '../../shared/svn.js'
8
9
  import type { TreeMenuRegistry } from './contextMenu.js'
9
10
 
10
11
  /** 面板可用的共享上下文(由 SidebarView 从 EditorView 注入,面板不直碰其内部)。 */
@@ -32,6 +33,20 @@ export interface SidebarCtx {
32
33
  fileMenuItems?: TreeMenuRegistry
33
34
  /** 「添加到对话」动作集(文件/文件夹引用注入对话输入框;缺省时菜单项降级提示)。 */
34
35
  addToConversation?: AddToConversation
36
+ /** SVN 能力状态(未加载/未检出为 null;SVN 菜单项显隐判定)。 */
37
+ svn?: SvnStatusPayload | null
38
+ /** SVN 变更清单(未加载为 null;文件树徽标与变更类菜单项判定)。 */
39
+ svnChanges?: SvnChangeEntry[] | null
40
+ /** 路径 → 变更条目(文件树徽标查表;由 svnStatus.svnChangeMapOf 产出)。 */
41
+ svnChangeMap?: Record<string, SvnChangeEntry>
42
+ /** 打开 SVN 基线差异视图(变更面板行内「与基线比较」;缺省时面板降级提示)。 */
43
+ openSvnDiff?: (path: string) => void
44
+ /** 重查 SVN 变更清单(CLI 更新/还原/加入版本控制后由菜单动作调用)。 */
45
+ refreshSvnChanges?: () => void
46
+ /** 打开 SVN 日志弹窗(P3;菜单/命令栏「查看日志」)。 */
47
+ openSvnLog?: (path?: string) => void
48
+ /** 确认对话框(破坏性动作守卫;缺省走 window.confirm,测试可注入)。 */
49
+ confirm?: (message: string) => boolean
35
50
  /** 面板动作反馈(如右键菜单操作结果 → 编辑区路径栏状态)。 */
36
51
  notify?: (message: string) => void
37
52
  }
@@ -45,6 +60,11 @@ export interface SidebarPanelDef {
45
60
  order?: number
46
61
  /** 活动栏徽标:返回数字/null;缺省不显示。 */
47
62
  badge?: (ctx: SidebarCtx) => number | null
63
+ /**
64
+ * 面板可用性守卫:返回 false 时活动栏图标与面板区都不出现(每次渲染求值)。
65
+ * 用于能力相关面板(如「SVN 变更」仅在受 SVN 管理时出现);缺省恒可见。
66
+ */
67
+ visible?: (ctx: SidebarCtx) => boolean
48
68
  /** 面板区渲染(ctx 每次渲染注入最新快照)。 */
49
69
  render: (ctx: SidebarCtx) => unknown
50
70
  }
@@ -23,6 +23,16 @@ let pending = null
23
23
  let registered = false
24
24
  let disposer = null
25
25
 
26
+ /**
27
+ * 片段 provider 注销器的全局挂点名。
28
+ *
29
+ * 为什么挂 window:`window.monaco` 由 loader 注入后**跨插件重载存活**,而模块级
30
+ * registered/disposer 会随 bundle 重新求值复位 —— 只判 registered 会在每次重载后
31
+ * 重复注册补全 provider(同一编辑器出现重复候补)。故注销器落 window 供跨代认领与清理。
32
+ * @author ddj 2026年09月18号
33
+ */
34
+ const SNIPPET_GLOBAL = '__edrvSnippetsDisposer__'
35
+
26
36
  /** 当前会话 id(补全按会话工作区叠加项目片段;EditorView 装配时注入)。 */
27
37
  let sessionId = null
28
38
 
@@ -102,6 +112,12 @@ export function entriesForLanguage(entries, languageId) {
102
112
  */
103
113
  export function registerSnippetProvider(monaco) {
104
114
  if (registered || !monaco?.languages?.registerCompletionItemProvider) return
115
+ // 跨重载守卫:上一代 bundle 注册的 provider 仍在存活 window.monaco 上(注销器已落 window)
116
+ const host = /* @__PURE__ */ (typeof window === 'undefined' ? undefined : window)
117
+ if (host && host[SNIPPET_GLOBAL]) {
118
+ registered = true
119
+ return
120
+ }
105
121
  const snippetKind = monaco.languages.CompletionItemKind?.Snippet
106
122
  const asSnippet = monaco.languages.CompletionItemInsertTextRule?.InsertAsSnippet
107
123
  // 缺少片段枚举(精简版 Monaco)时不注册:否则候选项会退化为纯文本插入,误导用户
@@ -135,6 +151,8 @@ export function registerSnippetProvider(monaco) {
135
151
  }
136
152
  },
137
153
  })
154
+ // 注销器落 window:跨重载认领(见 SNIPPET_GLOBAL 说明)
155
+ if (host) host[SNIPPET_GLOBAL] = disposer
138
156
  }
139
157
 
140
158
  /**
@@ -147,13 +165,17 @@ export function setupSnippets(monaco) {
147
165
  }
148
166
 
149
167
  /**
150
- * 卸载:注销 provider(插件热重载/卸载时调用)。
151
- * @author ddj 2026年09月10号
168
+ * 卸载:注销 provider 并复位状态(插件热重载/卸载时调用)。
169
+ * 兼容上一代 bundle 遗留的 window 注销器(模块级 disposer 为空时仍能清干净)。
170
+ * @author ddj 2026年09月10号 / 2026年09月18号
152
171
  */
153
172
  export function disposeSnippets() {
154
- if (typeof disposer === 'function') {
155
- try { disposer.dispose?.() } catch { /* 已注销 */ }
173
+ const host = /* @__PURE__ */ (typeof window === 'undefined' ? undefined : window)
174
+ const target = (typeof disposer === 'function' ? disposer : null) ?? (host ? host[SNIPPET_GLOBAL] : null)
175
+ if (target && typeof target.dispose === 'function') {
176
+ try { target.dispose() } catch { /* 已注销 */ }
156
177
  }
178
+ if (host) delete host[SNIPPET_GLOBAL]
157
179
  disposer = null
158
180
  registered = false
159
181
  invalidateSnippets()