dsh-vscode-mode 0.19.0 → 0.19.2

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.19.0",
3
+ "version": "0.19.2",
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 扩展市场安装),状态持久化到工作区旁车,界面状态(页签/光标/侧栏/编辑 Tab/导航历史)镜像到 DSH home 跨重启恢复",
5
5
  "icon": "assets/icon.svg",
6
6
  "keywords": [
@@ -10,6 +10,7 @@ import { openDeepLink } from './openFlow.js'
10
10
  import { disposeOpenWait } from './openReceipt.js'
11
11
  import { rpc } from './rpc.js'
12
12
  import { log } from './log.js'
13
+ import { createRequestId } from './requestId.js'
13
14
 
14
15
  /** toast 自隐时长。 */
15
16
  const TOAST_MS = 5000
@@ -68,7 +69,12 @@ function startPresencePoll(ctx: unknown): () => void {
68
69
  if (typeof window === 'undefined') return () => {}
69
70
  const marker = window as unknown as { __edrvExtPoll?: () => void }
70
71
  if (typeof marker.__edrvExtPoll === 'function') marker.__edrvExtPoll()
71
- const clientId = crypto.randomUUID()
72
+ const generatedId = createRequestId()
73
+ if (!generatedId) {
74
+ log.warn('安全随机源不可用,已停用外部打开心跳')
75
+ return () => {}
76
+ }
77
+ const clientId: string = generatedId
72
78
  let disposed = false
73
79
  let polling = false
74
80
  let executing = false
@@ -65,6 +65,8 @@ export const LANG_BY_EXT = {
65
65
  // 工程/配置文件(拿到正确语法分色,而非 plaintext)
66
66
  csproj: 'xml', props: 'xml', targets: 'xml', plist: 'xml',
67
67
  lua51: 'lua', luac: 'lua',
68
+ v: 'verilog', vh: 'verilog', sv: 'systemverilog', svh: 'systemverilog',
69
+ tcl: 'tcl', xdc: 'tcl', sdc: 'tcl', ucf: 'tcl',
68
70
  gitattributes: 'ini', editorconfig: 'ini', env: 'ini', properties: 'ini',
69
71
  json5: 'jsonc', log: 'plaintext', txt: 'plaintext',
70
72
  // 代码片段文件按 JSON 高亮(VS Code 同款:.code-snippets 是带注释的 JSON)
@@ -291,6 +291,17 @@ const COLOR_TOKEN_MAP = [
291
291
  /** 已应用的官方主题序号(模块级计数,保证主题 id 每次唯一)。 */
292
292
  let officialSeq = 0
293
293
 
294
+ /** @public @author ddj 2026年10月23号
295
+ * Read the host content font on editor creation, preserving the original line density.
296
+ * @returns Monaco font options; invalid or unavailable CSS falls back to 16px.
297
+ */
298
+ export function editorFont(): { fontSize: number; lineHeight: number } {
299
+ const raw = readCssVar('--dsh-content-font-size') ?? ''
300
+ const parsed = /^\d+(?:\.\d+)?px$/i.test(raw) ? Number(raw.slice(0, -2)) : NaN
301
+ const fontSize = Number.isFinite(parsed) && parsed > 0 ? parsed : 16
302
+ return { fontSize, lineHeight: Math.round(fontSize * 21 / 16) }
303
+ }
304
+
294
305
  /**
295
306
  * 读官方 CSS 变量(先 body 后文档根;未定义/空值/未解析 var() 返回 undefined)。
296
307
  * 依据运行态实测:DSH 的 --dsw-alias-* 定义在 body 上,仅 --shiki-token-* 在根。
@@ -1,4 +1,5 @@
1
1
  import { openEditorView } from './events.js'
2
+ import { createRequestId } from './requestId.js'
2
3
 
3
4
  import type { PlanDocument } from './planState.js'
4
5
 
@@ -41,7 +42,9 @@ export function finishOpen(requestId: string, success: boolean, error?: string):
41
42
  */
42
43
  export function requestOpen(path: string | null, line?: number, column?: number, sessionId = '', options: OpenOptions = {}): Promise<boolean> {
43
44
  if (typeof window === 'undefined' || !sessionId || options.signal?.aborted) return Promise.resolve(false)
44
- const request: EditorRequest = { requestId: crypto.randomUUID(), sessionId, path, line, column }
45
+ const requestId = createRequestId()
46
+ if (!requestId) return Promise.resolve(false)
47
+ const request: EditorRequest = { requestId, sessionId, path, line, column }
45
48
  if (options.preview !== undefined) request.preview = options.preview
46
49
  if (options.plan) request.plan = options.plan
47
50
  return new Promise((resolve) => {
@@ -0,0 +1,20 @@
1
+ /** @public @author ddj 2026年10月23号
2
+ * Generate an unpredictable UUID, including on insecure LAN HTTP origins.
3
+ * @returns UUID v4, or undefined when secure randomness is unavailable.
4
+ */
5
+ export function createRequestId(): string | undefined {
6
+ try {
7
+ if (typeof globalThis.crypto?.randomUUID === 'function') return globalThis.crypto.randomUUID()
8
+ } catch { /* Fall through to the cryptographic API available on HTTP origins. */ }
9
+
10
+ try {
11
+ const bytes = new Uint8Array(16)
12
+ globalThis.crypto.getRandomValues(bytes)
13
+ bytes[6] = (bytes[6] & 0x0f) | 0x40
14
+ bytes[8] = (bytes[8] & 0x3f) | 0x80
15
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')
16
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
17
+ } catch {
18
+ return undefined
19
+ }
20
+ }
@@ -35,6 +35,8 @@ const PREFETCH_MAX = 4
35
35
  const PREFETCH_EXCLUDED = new Set(['node_modules', '.git', '.hg', '.svn', '.pnpm', '.pnpm-store'])
36
36
  /** 外部文件变化重列去抖:一轮外部批量写入(如 agent 连写多文件)合并为一次重列。 */
37
37
  const FILE_CHANGE_DEBOUNCE_MS = 400
38
+ /** 兼容 host 返回裸文案或附带路径的缺失目录错误。 */
39
+ const MISSING_DIR_PREFIX = '目录不存在'
38
40
 
39
41
  // --region 行图标(官方原语:目录文件夹图标 + 文件类型图标;缺失时回落纯文本)
40
42
 
@@ -135,6 +137,24 @@ export function FileExplorer(props) {
135
137
  /** 渲染取数:内存态 → 本地条目缓存 → null(显示加载态)。 */
136
138
  const entriesOf = (rel) => dirsRef.current[rel] ?? entriesCacheGet(scope, rel) ?? null
137
139
 
140
+ /**
141
+ * @private
142
+ * @author ddj 2026年10月23号
143
+ * @description 移除已删除目录的展开记录,经现有防抖持久化自愈;不改请求 token,保留 finally 的加载态清理。
144
+ * @param rel 已确认缺失的非根相对目录
145
+ */
146
+ function dropDeadDir(rel) {
147
+ setExpanded(removeExpansion)
148
+
149
+ /** @private @author ddj 2026年10月23号 @param prev 当前展开映射。 @returns 移除失效目录后的展开映射。 */
150
+ function removeExpansion(prev) {
151
+ if (prev[rel] !== true) return prev
152
+ const next = Object.assign({}, prev)
153
+ delete next[rel]
154
+ return next
155
+ }
156
+ }
157
+
138
158
  /** 预取子目录:≤4 个、排除重型目录、缓存新鲜跳过、已加载跳过、不级联。 */
139
159
  const prefetchDirs = (rel, entries) => {
140
160
  const want = []
@@ -176,11 +196,14 @@ export function FileExplorer(props) {
176
196
  if (res.root) setRoot((prev) => prev || res.root)
177
197
  entriesCachePut(scope, rel, res.entries)
178
198
  if (doPrefetch) prefetchDirs(rel, res.entries)
179
- } else {
180
- // 有可渲染数据时静默(旧数据可能过期但可用);全无数据才报错
181
- if (!entriesOf(rel)) {
182
- setErrors((prev) => Object.assign({}, prev, { [rel]: res?.error ? String(res.error) : '读取目录失败' }))
199
+ } else if (!entriesOf(rel)) {
200
+ // 保留有缓存时的 SWR 降级;仅缺失的子目录自愈,根目录错误必须继续展示。
201
+ const message = res?.error ? String(res.error) : '读取目录失败'
202
+ if (rel && message.startsWith(MISSING_DIR_PREFIX)) {
203
+ dropDeadDir(rel)
204
+ return
183
205
  }
206
+ setErrors((prev) => Object.assign({}, prev, { [rel]: message }))
184
207
  }
185
208
  }).catch((e) => {
186
209
  if (scopeRef.current !== scope || tokensRef.current !== tokens || tokens[rel] !== token) return
@@ -475,7 +498,13 @@ export function FileExplorer(props) {
475
498
 
476
499
  const errorList = Object.entries(errors)
477
500
  // 跨端会话缺失:裸错误替换为友好标题 + 详情(重试仍保留,会话在本端可用后即恢复)
478
- const rawErrorText = errorList.length ? errorList[0][1] : null
501
+ let rawErrorText = null
502
+ if (errorList.length) {
503
+ const [errorRel, message] = errorList[0]
504
+ rawErrorText = message
505
+ // 路径只在展示层追加,RPC 业务错误与异常均可定位;兼容已带路径的 host。
506
+ if (errorRel && !message.endsWith(':' + errorRel)) rawErrorText += ':' + errorRel
507
+ }
479
508
  const sessionMissing = isSessionMissingError(rawErrorText)
480
509
  const errorText = rawErrorText === null
481
510
  ? null
@@ -97,6 +97,8 @@ export interface UiStateMirrorDeps {
97
97
  export interface UiStateMirror {
98
98
  /** 包装 localStorage 写口(写口不可用时返回 false)。 */
99
99
  install(): boolean
100
+ /** Release storage hooks and pending hydration work. */
101
+ dispose(): void
100
102
  /** 取回 host 镜像并回填本地缺失键,返回回填条数。 */
101
103
  hydrate(): Promise<number>
102
104
  /** 立即上送待推送批次,返回最终丢弃的键数(0 = 全部落盘;供卸载与测试驱动)。 */
@@ -194,6 +196,8 @@ export function createUiStateMirror(deps: UiStateMirrorDeps): UiStateMirror {
194
196
  let suppressed = 0
195
197
  let done = false
196
198
  let installed = false
199
+ let disposed = false
200
+ let restoreStorage: (() => void) | undefined
197
201
  let lastError = ''
198
202
  /** 水合尝试次数(失败重试;超过 `HYDRATE_RETRY_MAX` 放弃并解禁)。 */
199
203
  let attempts = 0
@@ -203,14 +207,19 @@ export function createUiStateMirror(deps: UiStateMirrorDeps): UiStateMirror {
203
207
 
204
208
  /** 记入待推送批次并安排去抖上送(水合期间或非镜像键 → 跳过)。 */
205
209
  function queue(key: string, value: string | null): void {
206
- if (suppressed > 0 || !isMirroredKey(key)) return
210
+ if (disposed || suppressed > 0 || !isMirroredKey(key)) return
207
211
  const bucket = isGlobalKey(key) ? pending.globals : pending.keys
208
212
  bucket[key] = value
209
213
  if (timer !== null) cancel(timer)
210
- timer = schedule(() => {
214
+ try {
215
+ timer = schedule(() => {
216
+ timer = null
217
+ if (!disposed) void flush()
218
+ }, FLUSH_DEBOUNCE_MS)
219
+ } catch (error) {
211
220
  timer = null
212
- void flush()
213
- }, FLUSH_DEBOUNCE_MS)
221
+ log('界面状态调度失败: ' + String(error))
222
+ }
214
223
  }
215
224
 
216
225
  /** 上送一批补丁(失败重试一次)。 */
@@ -273,7 +282,7 @@ export function createUiStateMirror(deps: UiStateMirrorDeps): UiStateMirror {
273
282
 
274
283
  /** @private @author ddj 2026年10月12号 @returns Durable refresh snapshot. @throws When state is unreadable, unsettled or rejected by Host. @sideEffects Resends current local state after waiting for inflight writes. */
275
284
  async function flushStrict(): Promise<void> {
276
- if (!settleDone || !installed) throw new Error('界面状态镜像尚未就绪')
285
+ if (disposed || !settleDone || !installed) throw new Error('界面状态镜像尚未就绪')
277
286
  if (inflight) await inflight
278
287
  // Read directly: the lenient key scanner intentionally hides storage errors.
279
288
  for (let index = 0; index < storage.length; index++) {
@@ -338,7 +347,7 @@ export function createUiStateMirror(deps: UiStateMirrorDeps): UiStateMirror {
338
347
  * 未结束前界面不得写存档,否则空初值会覆盖 host 上的真实状态。
339
348
  */
340
349
  function settle(): void {
341
- if (settleDone) return
350
+ if (disposed || settleDone) return
342
351
  settleDone = true
343
352
  emitReady()
344
353
  report('水合阶段结束(就绪;' + (done ? '已回填' : '未回填,退回纯 localStorage') + ')')
@@ -349,9 +358,11 @@ export function createUiStateMirror(deps: UiStateMirrorDeps): UiStateMirror {
349
358
  * @returns 本次回填键数(失败/重试中为 0)
350
359
  */
351
360
  async function hydrate(): Promise<number> {
361
+ if (disposed) return 0
352
362
  try {
353
363
  report('水合开始(origin=' + originOf() + ',第 ' + (attempts + 1) + ' 次)')
354
364
  const snapshot = await deps.fetch(deps.sessionIdOf?.())
365
+ if (disposed) return 0
355
366
  // 先搬运本 origin 既有状态(只含换 origin 前就存在的键),再回填缺失键:
356
367
  // 顺序反过来会把刚回填的键再回送一遍(值与 host 完全相同,纯属多余流量)。
357
368
  const seeded = seedLocal()
@@ -362,19 +373,24 @@ export function createUiStateMirror(deps: UiStateMirrorDeps): UiStateMirror {
362
373
  if (seeded > 0) {
363
374
  const lost = await flush()
364
375
  report('首次搬运输送完成:丢弃 ' + lost + ' 键')
365
- if (lost === 0) markSeeded()
376
+ if (!disposed && lost === 0) markSeeded()
366
377
  }
367
378
  settle()
368
379
  return filled
369
380
  } catch (error) {
381
+ if (disposed) return 0
370
382
  attempts++
371
383
  if (attempts < HYDRATE_RETRY_MAX) {
372
384
  report('水合失败(第 ' + attempts + ' 次,' + HYDRATE_RETRY_MS + 'ms 后重试): ' + String(error), 'warn')
373
- retryTimer = schedule(() => {
374
- retryTimer = null
375
- void hydrate()
376
- }, HYDRATE_RETRY_MS)
377
- return 0
385
+ try {
386
+ retryTimer = schedule(() => {
387
+ retryTimer = null
388
+ void hydrate()
389
+ }, HYDRATE_RETRY_MS)
390
+ return 0
391
+ } catch (scheduleError) {
392
+ report('水合重试调度失败: ' + String(scheduleError), 'warn')
393
+ }
378
394
  }
379
395
  report('水合放弃(重试 ' + attempts + ' 次仍失败): ' + String(error), 'warn')
380
396
  settle()
@@ -382,26 +398,58 @@ export function createUiStateMirror(deps: UiStateMirrorDeps): UiStateMirror {
382
398
  }
383
399
  }
384
400
 
401
+ /** @private @author ddj 2026年10月23号 Release timers and only this instance's storage hooks. */
402
+ function dispose(): void {
403
+ if (disposed) return
404
+ disposed = true
405
+ if (timer !== null) cancel(timer)
406
+ if (retryTimer !== null) cancel(retryTimer)
407
+ timer = null
408
+ retryTimer = null
409
+ restoreStorage?.()
410
+ installed = false
411
+ }
412
+
385
413
  return {
414
+ dispose,
386
415
  install(): boolean {
416
+ if (disposed) return false
387
417
  if (installed) return true
388
418
  try {
389
419
  const proto = Object.getPrototypeOf(storage) as Partial<UiStorage> | null
390
- const rawSet = proto?.setItem
391
- const rawRemove = proto?.removeItem
420
+ const rawSet = storage.setItem ?? proto?.setItem
421
+ const rawRemove = storage.removeItem ?? proto?.removeItem
392
422
  if (typeof rawSet !== 'function' || typeof rawRemove !== 'function') return false
393
- storage.setItem = function (key: string, value: string): void {
423
+ const setDescriptor = Object.getOwnPropertyDescriptor(storage, 'setItem')
424
+ const removeDescriptor = Object.getOwnPropertyDescriptor(storage, 'removeItem')
425
+ /** @private @author ddj 2026年10月23号 @param key Storage key. @param value Stored text. @sideEffects Queues a mirror update. */
426
+ function wrappedSet(key: string, value: string): void {
394
427
  rawSet.call(storage, key, value)
395
428
  queue(String(key), String(value))
396
429
  }
397
- storage.removeItem = function (key: string): void {
430
+ /** @private @author ddj 2026年10月23号 @param key Storage key. @sideEffects Queues a mirror deletion. */
431
+ function wrappedRemove(key: string): void {
398
432
  rawRemove.call(storage, key)
399
433
  queue(String(key), null)
400
434
  }
435
+ restoreStorage = () => {
436
+ if (storage.setItem === wrappedSet) {
437
+ if (setDescriptor) Object.defineProperty(storage, 'setItem', setDescriptor)
438
+ else delete (storage as Partial<UiStorage>).setItem
439
+ }
440
+ if (storage.removeItem === wrappedRemove) {
441
+ if (removeDescriptor) Object.defineProperty(storage, 'removeItem', removeDescriptor)
442
+ else delete (storage as Partial<UiStorage>).removeItem
443
+ }
444
+ }
445
+ storage.setItem = wrappedSet
446
+ storage.removeItem = wrappedRemove
401
447
  installed = true
402
448
  report('镜像已包装 localStorage 写口')
403
449
  return true
404
450
  } catch (error) {
451
+ restoreStorage?.()
452
+ installed = false
405
453
  report('界面状态镜像包装失败(退回纯 localStorage): ' + String(error), 'warn')
406
454
  return false
407
455
  }
@@ -500,7 +548,7 @@ async function fetchViaRpc(sessionId?: string): Promise<UiStateSnapshot> {
500
548
  * @returns 是否安装成功(存储不可用 → false,功能退回纯 localStorage)
501
549
  */
502
550
  export function installUiStatePersist(
503
- ctx: { get: (name: string) => unknown },
551
+ ctx: { get: (name: string) => unknown; effect: (setup: () => () => void, label?: string) => unknown },
504
552
  schedule: (fn: () => void, ms: number) => unknown,
505
553
  report?: (text: string, level?: string) => void,
506
554
  ): boolean {
@@ -518,6 +566,9 @@ export function installUiStatePersist(
518
566
  fetch: fetchViaRpc,
519
567
  sessionIdOf,
520
568
  schedule,
569
+ cancel: (handle) => {
570
+ if (typeof handle === 'function') handle()
571
+ },
521
572
  report: (text: string, level?: string) => reportUiStateEvent(text, level),
522
573
  log: (text: string) => {
523
574
  try { dbg(sessionIdOf(), 'ui-state: ' + text, 'warn') } catch (error) { /* 日志失败忽略 */ }
@@ -528,18 +579,28 @@ export function installUiStatePersist(
528
579
  return false
529
580
  }
530
581
  singleton = mirror
531
- try {
532
- if (typeof window !== 'undefined') {
533
- window.addEventListener('pagehide', () => { void mirror.flush() })
582
+ const pageHost = typeof window === 'undefined' ? undefined : window
583
+ /** @private @author ddj 2026年10月23号 Persist pending changes when the page is hidden. */
584
+ function onPageHide(): void { void mirror.flush() }
585
+ ctx.effect(() => {
586
+ pageHost?.addEventListener('pagehide', onPageHide)
587
+ return () => {
588
+ pageHost?.removeEventListener('pagehide', onPageHide)
589
+ mirror.dispose()
590
+ if (singleton !== mirror) return
591
+ singleton = null
592
+ hydrating = null
593
+ uiReady = false
594
+ reporter = null
534
595
  }
535
- } catch (error) { /* 无 window 环境忽略 */ }
596
+ }, 'vscode-mode: ui state lifecycle')
536
597
  hydrating = mirror.hydrate().then((filled) => {
537
- if (filled > 0 || mirror.settled()) uiReady = true
598
+ if (singleton === mirror && (filled > 0 || mirror.settled())) uiReady = true
538
599
  return filled
539
600
  })
540
601
  void hydrating.finally(() => {
541
602
  // 首轮结束后:已结束水合阶段才允许后续 hydrateUiState() 重新发起(重试由镜像内部承担)
542
- if (mirror.settled()) {
603
+ if (singleton === mirror && mirror.settled()) {
543
604
  uiReady = true
544
605
  hydrating = null
545
606
  }
@@ -49,7 +49,7 @@ import {
49
49
  import { useFileWatch } from './useFileWatch.js'
50
50
  import { createPdfPanel } from '../pdf/pdfPanel.js'
51
51
  import { SvnDiffPanel } from './SvnDiffPanel.js'
52
- import { applyOfficial, registerThemes, themeNameOf } from '../monaco/theme.js'
52
+ import { applyOfficial, editorFont, registerThemes, themeNameOf } from '../monaco/theme.js'
53
53
  import { createDiffRenderer } from '../monaco/diffRender.js'
54
54
  import { ST, callIdAttr, noopHunk, summarize } from '../state/records.js'
55
55
  import { diffRegions } from '../state/regions.js'
@@ -2392,9 +2392,7 @@ export function EditorView(props) {
2392
2392
  language: 'plaintext',
2393
2393
  theme: themeNameOf(),
2394
2394
  fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)',
2395
- // 16px/21px:与 md 预览渲染态同字号,切分页/切组时观感不再跳变(ddj 2026-10-16)
2396
- fontSize: 16,
2397
- lineHeight: 21,
2395
+ ...editorFont(),
2398
2396
  minimap: { enabled: !side, scale: 1 },
2399
2397
  glyphMargin: true,
2400
2398
  lineDecorationsWidth: 16,
@@ -3990,7 +3988,7 @@ export function EditorView(props) {
3990
3988
  if (result.version) { revRef.current[path] = result.version; recordBaseline(scope, path, result.version) }
3991
3989
  const editor = window.monaco.editor.create(host, { model, automaticLayout: true, minimap: { enabled: false }, scrollBeyondLastLine: false, glyphMargin: true,
3992
3990
  // 与焦点编辑器(ensureEditor)同观感参数:字号/行高/顶部留白(side 形态 6,tab 形态 8)/装饰宽度,切分页行号位置不跳变(ddj 2026-10-16)
3993
- fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)', fontSize: 16, lineHeight: 21,
3991
+ fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)', ...editorFont(),
3994
3992
  lineDecorationsWidth: 16, padding: { top: layoutRef.current === 'side' ? 6 : 8 } })
3995
3993
  const unbindGutter = bindBpGutter(editor, host, path)
3996
3994
  keeper = createViewKeeper(editor)
@@ -128,7 +128,7 @@ export const SNIPPET_LANGUAGES: readonly string[] = [
128
128
  'c', 'cpp', 'csharp', 'css', 'dart', 'dockerfile', 'go', 'html', 'ini', 'java',
129
129
  'javascript', 'json', 'jsonc', 'kotlin', 'less', 'lua', 'markdown', 'mdx', 'php',
130
130
  'plaintext', 'powershell', 'python', 'ruby', 'rust', 'scss', 'shell', 'sql',
131
- 'swift', 'typescript', 'xml', 'yaml',
131
+ 'swift', 'systemverilog', 'tcl', 'typescript', 'verilog', 'xml', 'yaml',
132
132
  ]
133
133
 
134
134
  /** 语言 id → 展示名(仅收录写法与 id 明显不同的;未收录的原样返回)。 */