dsh-vscode-mode 0.14.3 → 0.14.5

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.
@@ -1,190 +1,250 @@
1
1
  // @ts-nocheck
2
- /**
3
- * dsh-vscode-mode client — 行内差异自绘(新增行绿底 + glyph margin '+';删除块 view zone 红底)。
4
- * 迁移自原 src/client/index.ts 的 diff-render effect,语义不改。
5
- * 作者 ddj 2026-08-20
6
- */
2
+ /** 行内差异:共享色盘、删除代码高亮与原行号。 @author ddj 2026年10月09号 */
3
+ import { detectColorScheme, officialThemeOf, themeNameOf } from './theme.js'
4
+
5
+ // #region 显示数据
6
+ /** @private @author ddj 2026年10月09号 @param regions 待显示区域 @returns 内容敏感且无分隔符碰撞的展示键 */
7
+ function renderKey(regions) {
8
+ return JSON.stringify(regions.map((r) => [r.callId, r.idx, r.start, r.end, r.oldStart, r.create, r.oldLines, r.newLines]))
9
+ }
10
+
11
+ /** @private @author ddj 2026年10月09号 @param node 接收样式的节点 @param colors Monaco 颜色 @returns 设置插件局部变量;不覆盖官方令牌 */
12
+ function setDiffColors(node, colors) {
13
+ if (!node) return
14
+ node.style.setProperty('--edrv-diff-add', colors['diffEditor.insertedLineBackground'])
15
+ node.style.setProperty('--edrv-diff-del', colors['diffEditor.removedLineBackground'])
16
+ node.style.setProperty('--edrv-diff-fg', colors['editor.foreground'] || '#1e1e1e')
17
+ node.style.setProperty('--edrv-diff-number', colors['editorLineNumber.foreground'] || '#6c6c6c')
18
+ }
19
+
20
+ /** @private @author ddj 2026年10月09号 @param text 源码行 @returns 安全纯文本行;不把源码写入 HTML */
21
+ function codeRow(text) {
22
+ const row = document.createElement('div')
23
+ row.className = 'edrv-del-row'
24
+ const span = document.createElement('span')
25
+ span.className = 'edrv-del-text'
26
+ span.textContent = text
27
+ row.appendChild(span)
28
+ return row
29
+ }
30
+
31
+ /** @private @author ddj 2026年10月09号 @param line 原行号(缺失则占位) @returns 旧行号与减号行 */
32
+ function marginRow(line) {
33
+ const row = document.createElement('div')
34
+ row.className = 'edrv-del-margin-row'
35
+ const number = document.createElement('span')
36
+ number.className = 'edrv-del-number'
37
+ number.textContent = line === undefined ? '·' : String(line)
38
+ if (line === undefined) number.title = '历史记录缺少可验证的原行号'
39
+ const marker = document.createElement('span')
40
+ marker.className = 'edrv-del-marker'
41
+ marker.textContent = '−'
42
+ row.appendChild(number)
43
+ row.appendChild(marker)
44
+ return row
45
+ }
46
+
47
+ /** @private @author ddj 2026年10月09号 @param region 差异区域 @returns 正文与边栏节点;Monaco 管理两者滚动 */
48
+ function zoneOf(region) {
49
+ const domNode = document.createElement('div')
50
+ domNode.className = 'edrv-del-zone'
51
+ domNode.dataset.edrvHunk = String(region.callId) + ':' + region.idx
52
+ const marginDomNode = document.createElement('div')
53
+ marginDomNode.className = 'edrv-del-margin'
54
+ const rows = []
55
+ for (let index = 0; index < region.oldLines.length; index++) {
56
+ const row = codeRow(region.oldLines[index])
57
+ domNode.appendChild(row)
58
+ rows.push(row.firstChild)
59
+ marginDomNode.appendChild(marginRow(region.oldStart === undefined ? undefined : region.oldStart + index))
60
+ }
61
+ return { domNode, marginDomNode, rows, lines: region.oldLines, after: Math.max(0, region.start - 1) }
62
+ }
63
+ // #endregion
7
64
 
8
65
  /**
9
- * 差异渲染器(每 EditorView 挂载一个,内部持有 zone/decoration 状态)。
10
- * @param log 诊断日志函数((sessionId, text) => void)
66
+ * 每个编辑器视图一个渲染器;持有并释放装饰、区域与监听。
67
+ * @public @author ddj 2026年10月09号
68
+ * @param log 诊断日志函数(失败不影响展示)
69
+ * @returns render、refresh 与 dispose;不修改审查决策或编辑器模型
11
70
  */
12
71
  export function createDiffRenderer(log) {
13
- let viewZoneIds = []
72
+ let owner = null
73
+ let api = null
74
+ let model = null
75
+ let language = null
76
+ let session = null
77
+ let pending = []
78
+ let zones = []
14
79
  let decorations = []
15
- /** 上次已渲染的差异指纹(callId:idx@start-end:oldLen/newLen 串):内容未变时跳过重建。 */
80
+ let listeners = []
16
81
  let lastKey = null
82
+ let generation = 0
83
+ let theme = themeNameOf()
17
84
 
18
- /**
19
- * 渲染当前 pending 差异:绿底 decoration + 删除块 view zones + '-' 号 overlay(滚动同步)。
20
- * @param monaco window.monaco
21
- * @param editor Monaco 编辑器实例
22
- * @param pendingRegions 待处理差异区域
23
- * @param sessionId 会话 id
24
- */
25
- const render = (monaco, editor, pendingRegions, sessionId) => {
26
- // 空渲染且无残留(5s 轮询 setRecords 常触发):无需清理时直接跳过,
27
- // 不打 begin 日志、不做无谓 DOM 操作;有 zone/decoration 残留时仍需走清理。
28
- if (!pendingRegions.length && viewZoneIds.length === 0 && decorations.length === 0) return
29
- const renderT0 = Date.now()
30
- const callIdAttr = (callId, idx) => String(callId) + ':' + String(idx)
31
- // 指纹相同 = 内容未变(轮询/引用空转/重复点击触发的重渲染):残留 DOM 已在位,
32
- // 直接跳过整轮重建(view zones 拆建 / overlay DOM / 50ms 校正),消除同区域重复渲染成本。
33
- const key = pendingRegions.map((r) => callIdAttr(r.callId, r.idx) + '@' + (r.start ?? '?') + '-' + (r.end ?? '?') + ':' + (r.oldLines ? r.oldLines.length : 0) + '/' + (r.newLines ? r.newLines.length : 0)).join('|')
34
- if (key === lastKey) return
35
- lastKey = key
36
- log(sessionId, '[diff-render] begin regs=' + pendingRegions.length + ' ' + pendingRegions.map((r) => callIdAttr(r.callId, r.idx) + '@' + (r.start ?? '?') + '-' + (r.end ?? '?') + '(-' + (r.oldLines ? r.oldLines.length : 0) + '/+' + (r.newLines ? r.newLines.length : 0) + ')').join('|'))
37
-
38
- const decos = []
39
- for (const r of pendingRegions) {
40
- if (r.start === undefined || r.end === undefined) continue
41
- if (r.newLines && r.newLines.length) {
42
- decos.push({
43
- range: new monaco.Range(r.start, 1, Math.max(r.start, r.end - 1), 1),
44
- options: { isWholeLine: true, className: 'edrv-mn-add-line', linesDecorationsClassName: 'edrv-mn-gutter-add' },
45
- })
85
+ // #region 生命周期
86
+ /** @private @author ddj 2026年10月09号 清除区域与异步代次;保留编辑器和监听以便刷新。 */
87
+ function clear() {
88
+ generation++
89
+ lastKey = null
90
+ if (owner) {
91
+ try { decorations = owner.deltaDecorations(decorations, []) } catch { decorations = [] }
92
+ /** @author ddj 2026年10月09号 @param accessor Monaco 区域事务 */
93
+ function removeZones(accessor) {
94
+ for (const zone of zones) accessor.removeZone(zone.id)
46
95
  }
96
+ try { owner.changeViewZones(removeZones) } catch { /* 编辑器可能已销毁。 */ }
47
97
  }
48
- decorations = editor.deltaDecorations(decorations, decos)
49
-
50
- // 删除块 view zones(红底 + 正文;- 号由独立 overlay 层渲染,定位在 decoration 列与 + 同列,
51
- // 不受 view zone 容器定位/裁剪影响;滚动用 translateY 同步)
52
- let createdZones = []
53
- editor.changeViewZones((accessor) => {
54
- for (const id of viewZoneIds) { try { accessor.removeZone(id) } catch (e) { /* 已移除 */ } }
55
- viewZoneIds = []
56
- const zones = []
57
- for (const r of pendingRegions) {
58
- if (r.create) continue
59
- const oldLines = r.oldLines && r.oldLines.length ? r.oldLines : null
60
- if (!oldLines) continue
61
- const after = Math.max(0, (r.start ?? 1) - 1)
62
- const domNode = document.createElement('div')
63
- domNode.className = 'edrv-del-zone'
64
- domNode.dataset.edrvHunk = callIdAttr(r.callId, r.idx)
65
- for (const t of oldLines) {
66
- const row = document.createElement('div')
67
- row.className = 'edrv-del-row'
68
- const text = document.createElement('span')
69
- text.className = 'edrv-del-text'
70
- text.textContent = t
71
- row.appendChild(text)
72
- domNode.appendChild(row)
73
- }
74
- const id = accessor.addZone({
75
- afterLineNumber: after,
76
- heightInLines: oldLines.length,
77
- domNode,
78
- suppressMouseDown: false,
79
- })
80
- viewZoneIds.push(id)
81
- zones.push({ domNode, after, n: oldLines.length })
82
- }
83
- createdZones = zones
84
- })
85
-
86
- if (createdZones.length) {
87
- const root = editor.getDomNode()
88
- if (root) {
89
- let overlay = root.querySelector('.edrv-minus-overlay')
90
- if (!overlay) {
91
- overlay = document.createElement('div')
92
- overlay.className = 'edrv-minus-overlay'
93
- root.appendChild(overlay)
94
- }
95
- overlay.innerHTML = ''
96
- const lineH = editor.getOption(monaco.editor.EditorOption.lineHeight) || 20
97
- let li = null
98
- try { li = editor.getLayoutInfo() } catch (e) { /* ignore */ }
99
- const rawLeft = li ? li.decorationsLeft : 0
100
- const width = li && li.decorationsWidth > 0 ? li.decorationsWidth : 16
101
- // decorationsLeft 是 Monaco 内部坐标,需限制在宿主根节点内,避免窄窗口/边框
102
- // 或布局尚未完成时把减号绘制到编辑器左、右边界之外。
103
- const maxLeft = Math.max(0, root.clientWidth - width)
104
- // 字符本身有少量字体侧向留白;给标记列留出 4px 内缩,避免减号半个
105
- // 字符落到 Monaco 根节点左边框外,同时仍限制右侧不越界。
106
- const markerInset = 12
107
- const left = Math.max(0, Math.min(maxLeft, rawLeft + markerInset))
108
- const lineCount = editor.getModel().getLineCount()
109
- let itemIdx = 0
110
- let zi = 0
111
- for (const z of createdZones) {
112
- // 用 Monaco 坐标 API 计算 view zone 顶部,避免离屏 zone DOM 未布局导致 getBoundingClientRect 为 0
113
- const anchor = Math.min(z.after + 1, lineCount)
114
- let zoneBottom = editor.getTopForLineNumber(anchor)
115
- if (z.after >= lineCount) zoneBottom += lineH
116
- const zoneTop = zoneBottom - z.n * lineH
117
- for (let i = 0; i < z.n; i++) {
118
- const item = document.createElement('div')
119
- item.className = 'edrv-minus-item'
120
- // 使用 CSS 线段代替文本字符,避免不同字体的侧向留白把 “-” 绘制到边界外。
121
- item.textContent = ''
122
- item.style.width = width + 'px'
123
- item.style.height = lineH + 'px'
124
- item.dataset.zone = String(zi)
125
- item.dataset.row = String(i)
126
- item.style.top = (zoneTop + i * lineH) + 'px'
127
- item.style.left = left + 'px'
128
- overlay.appendChild(item)
129
- itemIdx++
130
- }
131
- zi++
132
- }
133
- log(sessionId, '[diff-render] placed zones=' + createdZones.length + ' items=' + itemIdx + ' left=' + left + ' w=' + width + ' t+' + (Date.now() - renderT0) + 'ms')
134
- // 滚动同步:overlay 随内容滚动(内容坐标 - scrollTop)
135
- const syncScroll = () => {
136
- const s2 = editor.getScrollTop() || 0
137
- if (overlay) overlay.style.transform = 'translateY(' + (-s2) + 'px)'
138
- }
139
- syncScroll()
140
- overlay.__edrvSync = syncScroll
141
- if (!overlay.__edrvBound) {
142
- overlay.__edrvBound = true
143
- editor.onDidScrollChange(syncScroll)
144
- }
145
- // 已可见的 zone 用真实 DOM 坐标做一次微调(离屏 zone 仍保留坐标 API 位置)
146
- setTimeout(() => {
147
- try {
148
- const edRect = root.getBoundingClientRect()
149
- const st = editor.getScrollTop() || 0
150
- const items = overlay.querySelectorAll('.edrv-minus-item')
151
- let correctedLeft = left
152
- try {
153
- const layout = editor.getLayoutInfo()
154
- const markerWidth = layout.decorationsWidth > 0 ? layout.decorationsWidth : width
155
- correctedLeft = Math.max(0, Math.min(Math.max(0, root.clientWidth - markerWidth), (layout.decorationsLeft || 0) + 12))
156
- } catch (e) { /* 保留首次布局计算结果 */ }
157
- for (const item of items) {
158
- item.style.left = correctedLeft + 'px'
159
- const zIndex = Number(item.dataset.zone)
160
- const rIndex = Number(item.dataset.row)
161
- const z = createdZones[zIndex]
162
- if (!z || !z.domNode.isConnected) continue
163
- const rowEl = z.domNode.querySelectorAll('.edrv-del-row')[rIndex]
164
- if (!rowEl) continue
165
- const rowRect = rowEl.getBoundingClientRect()
166
- if (rowRect.height > 0) {
167
- item.style.top = (rowRect.top - edRect.top + st) + 'px'
168
- }
169
- }
170
- } catch (e) { log(sessionId, '[diff-render] correct fail ' + String(e)) }
171
- }, 50)
172
- }
173
- } else {
174
- const root = editor.getDomNode()
175
- const overlay = root && root.querySelector('.edrv-minus-overlay')
176
- if (overlay) overlay.innerHTML = ''
98
+ zones = []
99
+ }
100
+
101
+ /** @public @author ddj 2026年10月09号 释放所有本渲染器拥有的资源;允许之后重新绑定。 */
102
+ function dispose() {
103
+ for (const listener of listeners) listener.dispose()
104
+ listeners = []
105
+ clear()
106
+ owner = model = api = null
107
+ pending = []
108
+ }
109
+
110
+ /** @private @author ddj 2026年10月09号 模型切换时丢弃旧区域,等待 React 提供新模型的区域。 */
111
+ function modelChanged() {
112
+ clear()
113
+ model = owner.getModel()
114
+ pending = []
115
+ }
116
+
117
+ /** @private @author ddj 2026年10月09号 @param monaco Monaco API @param editor 编辑器;只订阅一次 */
118
+ function bind(monaco, editor) {
119
+ if (owner === editor) return
120
+ dispose()
121
+ owner = editor
122
+ api = monaco
123
+ for (const event of ['onDidChangeConfiguration', 'onDidChangeModelLanguage', 'onDidChangeModelOptions']) {
124
+ if (typeof owner[event] === 'function') listeners.push(owner[event](refresh))
177
125
  }
178
- // #region debug log
179
- try { log(sessionId, '[DEBUG diffRender] regions=' + pendingRegions.length + ' zones=' + createdZones.length + ' ms=' + (Date.now() - renderT0)) } catch (e) { /* 诊断不可影响渲染 */ }
180
- // #endregion
126
+ if (owner.onDidLayoutChange) listeners.push(owner.onDidLayoutChange(reflow))
127
+ if (owner.onDidChangeModel) listeners.push(owner.onDidChangeModel(modelChanged))
128
+ if (owner.onDidDispose) listeners.push(owner.onDidDispose(dispose))
129
+ }
130
+ // #endregion
131
+
132
+ // #region 着色与布局
133
+ /** @private @author ddj 2026年10月09号 @param zone 删除区域 @param epoch 发起代次;拒绝旧模型、语言和主题结果 */
134
+ async function colorZone(zone, epoch) {
135
+ if (typeof api.editor.colorize !== 'function') return
136
+ const sourceModel = model
137
+ const sourceLang = language
138
+ const sourceTheme = theme
139
+ const tabSize = sourceModel.getOptions?.().tabSize || 4
140
+ try {
141
+ const html = await api.editor.colorize(zone.lines.join('\n'), sourceLang, { tabSize })
142
+ if (epoch !== generation || owner?.getModel() !== sourceModel || sourceModel.getLanguageId() !== sourceLang || theme !== sourceTheme) return
143
+ // Monaco 生成的 HTML 已转义源码;逐行分配仅使用其输出,不拼接用户源码。
144
+ const lines = html.split(/<br\s*\/?\s*>/i)
145
+ if (lines.length === zone.rows.length + 1 && lines.at(-1) === '') lines.pop()
146
+ if (lines.length !== zone.rows.length) return
147
+ for (let index = 0; index < lines.length; index++) zone.rows[index].innerHTML = lines[index]
148
+ } catch { /* 保留立即显示的安全纯文本。 */ }
181
149
  }
182
150
 
183
- /** 卸载清理(EditorView unmount 时调用)。 */
184
- const dispose = () => {
185
- viewZoneIds = []
186
- decorations = []
151
+ /** @private @author ddj 2026年10月09号 @param zone 区域 @param colors 当前主题颜色;跟随 Monaco 实际字体/布局 */
152
+ function layoutZone(zone, colors) {
153
+ const layout = owner.getLayoutInfo()
154
+ const font = owner.getOption(api.editor.EditorOption.fontInfo)
155
+ const lineHeight = owner.getOption(api.editor.EditorOption.lineHeight) || 20
156
+ const tabSize = model.getOptions?.().tabSize || 4
157
+ owner.applyFontInfo?.(zone.domNode)
158
+ owner.applyFontInfo?.(zone.marginDomNode)
159
+ setDiffColors(zone.domNode, colors)
160
+ setDiffColors(zone.marginDomNode, colors)
161
+ zone.domNode.style.setProperty('--edrv-line-height', lineHeight + 'px')
162
+ zone.domNode.style.tabSize = String(tabSize)
163
+ zone.marginDomNode.style.setProperty('--edrv-line-height', lineHeight + 'px')
164
+ zone.marginDomNode.style.setProperty('--edrv-number-left', layout.lineNumbersLeft + 'px')
165
+ zone.marginDomNode.style.setProperty('--edrv-number-width', layout.lineNumbersWidth + 'px')
166
+ zone.marginDomNode.style.setProperty('--edrv-marker-left', layout.decorationsLeft + 'px')
167
+ zone.marginDomNode.style.setProperty('--edrv-marker-width', layout.decorationsWidth + 'px')
168
+ let width = 0
169
+ for (const line of zone.lines) width = Math.max(width, line.replace(/\t/g, ' '.repeat(tabSize)).length)
170
+ // 中文/宽字符不能按半宽估算;宁可保留余量,也不截断旧代码横向滚动范围。
171
+ const charWidth = Math.max(font?.typicalHalfwidthCharacterWidth || 8, font?.typicalFullwidthCharacterWidth || 0)
172
+ return width * charWidth + 24
173
+ }
174
+
175
+ /** @private @author ddj 2026年10月09号 容器大小变化只更新边栏几何,不重新着色/重建区域。 */
176
+ function reflow() {
177
+ if (!owner || !model) return
178
+ const colors = officialThemeOf(detectColorScheme()).colors
179
+ for (const zone of zones) layoutZone(zone, colors)
180
+ }
181
+
182
+ /** @private @author ddj 2026年10月09号 @param accessor Monaco 区域事务 @param colors 共享色盘 */
183
+ function addZones(accessor, colors) {
184
+ for (const region of pending) {
185
+ if (region.create || region.start === undefined || !region.oldLines?.length) continue
186
+ const zone = zoneOf(region)
187
+ const minWidthInPx = layoutZone(zone, colors)
188
+ zone.id = accessor.addZone({
189
+ afterLineNumber: zone.after, heightInLines: zone.lines.length,
190
+ domNode: zone.domNode, marginDomNode: zone.marginDomNode,
191
+ minWidthInPx, suppressMouseDown: false,
192
+ })
193
+ zones.push(zone)
194
+ void colorZone(zone, generation)
195
+ }
187
196
  }
188
197
 
189
- return { render, dispose }
198
+ /** @private @author ddj 2026年10月09号 @returns 新增行整行背景和加号装饰 */
199
+ function addDecorations() {
200
+ const out = []
201
+ for (const region of pending) {
202
+ if (region.start === undefined || region.end === undefined || !region.newLines?.length) continue
203
+ out.push({
204
+ range: new api.Range(region.start, 1, Math.max(region.start, region.end - 1), 1),
205
+ options: { isWholeLine: true, className: 'edrv-mn-add-line', marginClassName: 'edrv-mn-add-line', linesDecorationsClassName: 'edrv-mn-gutter-add' },
206
+ })
207
+ }
208
+ return out
209
+ }
210
+ // #endregion
211
+
212
+ // #region 渲染入口
213
+ /** @private @author ddj 2026年10月09号 按内容/模型/语言展示;空区域也能清除残留。 */
214
+ function paint() {
215
+ if (!owner) return
216
+ const nextModel = owner.getModel()
217
+ if (!nextModel) { clear(); return }
218
+ const nextLang = nextModel.getLanguageId()
219
+ const key = renderKey(pending)
220
+ if (lastKey === key && model === nextModel && language === nextLang) return
221
+ clear()
222
+ model = nextModel
223
+ language = nextLang
224
+ lastKey = key
225
+ const colors = officialThemeOf(detectColorScheme()).colors
226
+ setDiffColors(owner.getDomNode(), colors)
227
+ decorations = owner.deltaDecorations([], addDecorations())
228
+ /** @author ddj 2026年10月09号 @param accessor Monaco 区域事务 */
229
+ function mountZones(accessor) { addZones(accessor, colors) }
230
+ owner.changeViewZones(mountZones)
231
+ try { log?.(session, '[diff-render] regions=' + pending.length + ' zones=' + zones.length) } catch { /* 诊断不可影响渲染。 */ }
232
+ }
233
+
234
+ /** @public @author ddj 2026年10月09号 @param themeId 新应用的主题名(事件参数不作为主题名) 更新字体、语言或主题后重新着色。 */
235
+ function refresh(themeId) {
236
+ if (typeof themeId === 'string') theme = themeId
237
+ lastKey = null
238
+ paint()
239
+ }
240
+
241
+ /** @public @author ddj 2026年10月09号 @param monaco Monaco API @param editor 编辑器 @param regions 待显示区域 @param sessionId 会话 id */
242
+ function render(monaco, editor, regions, sessionId) {
243
+ bind(monaco, editor)
244
+ pending = regions
245
+ session = sessionId
246
+ paint()
247
+ }
248
+ // #endregion
249
+ return { render, refresh, dispose }
190
250
  }
@@ -118,6 +118,30 @@ const LIGHT_RULES = [
118
118
  { token: 'invalid', foreground: 'cd3131' },
119
119
  ]
120
120
 
121
+ /** 截图对齐回落色盘;未取得官方差异令牌,不宣称这些值为官方精确色值。 */
122
+ const DIFF_PALETTE = {
123
+ light: ['#e6f4e8', '#fbe7e3', '#b7e3bf80', '#f1b1a780'],
124
+ dark: ['#203b2c', '#482b2b', '#2d684880', '#8c454580'],
125
+ }
126
+
127
+ /**
128
+ * 行内与 Monaco 差异视图共享色盘,避免通用 hover 令牌覆盖增删语义。
129
+ * @public @author ddj 2026年10月09号
130
+ * @param scheme 明暗主题
131
+ * @returns Monaco 差异颜色键;调用方可安全修改返回对象
132
+ */
133
+ export function diffColorsOf(scheme) {
134
+ const [added, removed, addText, delText] = DIFF_PALETTE[scheme === 'light' ? 'light' : 'dark']
135
+ return {
136
+ 'diffEditor.insertedLineBackground': added,
137
+ 'diffEditor.removedLineBackground': removed,
138
+ 'diffEditor.insertedTextBackground': addText,
139
+ 'diffEditor.removedTextBackground': delText,
140
+ 'diffEditorGutter.insertedLineBackground': added,
141
+ 'diffEditorGutter.removedLineBackground': removed,
142
+ }
143
+ }
144
+
121
145
  /** 编辑器 UI 配色(与语法配色同套,保证 gutter/minimap/selection 一致)。 */
122
146
  const DARK_COLORS = {
123
147
  'editor.background': '#1e1e1e',
@@ -137,14 +161,7 @@ const DARK_COLORS = {
137
161
  'scrollbarSlider.background': '#79797966',
138
162
  'scrollbarSlider.hoverBackground': '#646464b3',
139
163
  'scrollbarSlider.activeBackground': '#bfbfbf66',
140
- // 差异视图色块(本仓库 vendored Monaco 无 .line-insert 静态背景规则,靠主题键保证;
141
- // 色相取自研差异 UI 同源:绿 #0f9d58 / 红 #d9534f,alpha 提到肉眼可辨)
142
- 'diffEditor.insertedLineBackground': '#0f9d5830',
143
- 'diffEditor.removedLineBackground': '#d9534f2e',
144
- 'diffEditor.insertedTextBackground': '#0f9d5833',
145
- 'diffEditor.removedTextBackground': '#d9534f40',
146
- 'diffEditorGutter.insertedLineBackground': '#0f9d5859',
147
- 'diffEditorGutter.removedLineBackground': '#d9534f59',
164
+ ...diffColorsOf('dark'),
148
165
  }
149
166
 
150
167
  const LIGHT_COLORS = {
@@ -165,13 +182,7 @@ const LIGHT_COLORS = {
165
182
  'scrollbarSlider.background': '#64646466',
166
183
  'scrollbarSlider.hoverBackground': '#646464b3',
167
184
  'scrollbarSlider.activeBackground': '#00000099',
168
- // 差异视图色块(同上:主题键保证整行背景可见;色相/透明度与暗色一致)
169
- 'diffEditor.insertedLineBackground': '#0f9d5830',
170
- 'diffEditor.removedLineBackground': '#d9534f2e',
171
- 'diffEditor.insertedTextBackground': '#0f9d5833',
172
- 'diffEditor.removedTextBackground': '#d9534f40',
173
- 'diffEditorGutter.insertedLineBackground': '#0f9d5859',
174
- 'diffEditorGutter.removedLineBackground': '#d9534f59',
185
+ ...diffColorsOf('light'),
175
186
  }
176
187
 
177
188
  /**
@@ -60,6 +60,8 @@ export function deferToOfficial(path: unknown): boolean {
60
60
  export interface OfficialTabParams {
61
61
  openPath?: string
62
62
  focusDiff?: boolean
63
+ /** Open Markdown in rendered mode; explicit line navigation still selects source. */
64
+ preview?: boolean
63
65
  /** 目标行号(行引用/工具行跳转透传;缺省仅打开文件)。 */
64
66
  line?: number
65
67
  }
@@ -72,7 +74,7 @@ export interface SidebarRightTabsLike {
72
74
  /** sidebarRight 服务的最小结构面(openTab 页路由 + openResource 资源路由 + 可选查询/定向打开)。 */
73
75
  export interface SidebarRightServiceLike {
74
76
  openTab: (kind: string, options?: { params?: unknown }) => void
75
- openResource?: (address: string, options?: { params?: unknown }) => void
77
+ openResource?: (address: string, options?: { params?: unknown; kind?: string }) => void
76
78
  /** 挂载会话的激活 Tab(判读编辑 Tab 是否已激活;无 seat 时 undefined)。 */
77
79
  active?: () => { kind?: string } | undefined
78
80
  /** 向指定会话的停靠面打开页类型(store 未 adopt 时 no-op)。 */
@@ -226,16 +228,19 @@ export function buildFileAddress(path: string, sessionId?: string): string {
226
228
  * @param service 官方 sidebarRight 服务(缺失/缺 openTab 返回 false)
227
229
  * @param path 目标文件路径(空值返回 false)
228
230
  * @param line 可选行号(透传给编辑器页签导航参数)
231
+ * @param preview 可选 Markdown 渲染模式(行号导航优先源码)
229
232
  * @returns 转发是否成功(openTab 抛错返回 false,由调用方决定重试或兜底)
230
233
  */
231
234
  export function forwardToEditor(
232
235
  service: SidebarRightServiceLike | undefined | null,
233
236
  path: string | null,
234
237
  line?: number,
238
+ preview?: boolean,
235
239
  ): boolean {
236
240
  if (!service || typeof service.openTab !== 'function' || !path) return false
237
241
  const params: OfficialTabParams = { openPath: path }
238
242
  if (line !== undefined) params.line = line
243
+ if (preview !== undefined) params.preview = preview
239
244
  try {
240
245
  // 官方语义:页类型恒去重,故转发只会聚焦既有编辑器页签,不产生新实例
241
246
  service.openTab(OFFICIAL_TAB_KIND, { params })
@@ -1,6 +1,9 @@
1
1
  import { openEditorView } from './events.js'
2
2
 
3
- export interface EditorRequest { requestId: string; sessionId: string; path: string | null; line?: number; column?: number }
3
+ import type { PlanDocument } from './planState.js'
4
+
5
+ export interface EditorRequest { requestId: string; sessionId: string; path: string | null; line?: number; column?: number; preview?: boolean; plan?: PlanDocument }
6
+ export interface OpenOptions { preview?: boolean; plan?: PlanDocument; signal?: AbortSignal }
4
7
  interface Waiter { request: EditorRequest; finish: (success: boolean) => void; claimed?: boolean }
5
8
  const waiters = new Map<string, Waiter>()
6
9
 
@@ -33,19 +36,26 @@ export function finishOpen(requestId: string, success: boolean, error?: string):
33
36
  * @param line Optional target line.
34
37
  * @param column Optional target column.
35
38
  * @param sessionId Owning session, required for routing.
39
+ * @param options Explicit preview, read-only plan document and optional cancellation signal.
36
40
  * @returns Actual completion; timeout and disposal are false.
37
41
  */
38
- export function requestOpen(path: string | null, line?: number, column?: number, sessionId = ''): Promise<boolean> {
39
- if (typeof window === 'undefined' || !sessionId) return Promise.resolve(false)
42
+ export function requestOpen(path: string | null, line?: number, column?: number, sessionId = '', options: OpenOptions = {}): Promise<boolean> {
43
+ if (typeof window === 'undefined' || !sessionId || options.signal?.aborted) return Promise.resolve(false)
40
44
  const request: EditorRequest = { requestId: crypto.randomUUID(), sessionId, path, line, column }
45
+ if (options.preview !== undefined) request.preview = options.preview
46
+ if (options.plan) request.plan = options.plan
41
47
  return new Promise((resolve) => {
42
48
  let timer: ReturnType<typeof setTimeout>
43
49
  /** @private @author ddj 2026年09月28号 Complete once and release the deadline. */
44
50
  function finish(success: boolean): void {
45
51
  clearTimeout(timer)
52
+ options.signal?.removeEventListener('abort', abort)
46
53
  waiters.delete(request.requestId)
47
54
  resolve(success)
48
55
  }
56
+ /** @private @author ddj 2026年10月08号 Cancel a stale plan open on scope change or disposal. */
57
+ function abort(): void { finish(false) }
58
+ options.signal?.addEventListener('abort', abort, { once: true })
49
59
  waiters.set(request.requestId, { request, finish })
50
60
  timer = setTimeout(() => finish(false), 20_000)
51
61
  openEditorView(null)