dsh-vscode-mode 0.14.4 → 0.14.6
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/README.md +14 -4
- package/lib/client.js +1127 -265
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +2 -0
- package/src/client/monaco/diffRender.ts +234 -174
- package/src/client/monaco/theme.ts +26 -15
- package/src/client/officialSidebar.ts +6 -1
- package/src/client/openReceipt.ts +13 -3
- package/src/client/planBridge.ts +178 -0
- package/src/client/planState.ts +129 -0
- package/src/client/receiptState.ts +1 -0
- package/src/client/state/editorFindState.ts +102 -0
- package/src/client/state/editorViewState.ts +56 -0
- package/src/client/state/regions.ts +83 -3
- package/src/client/styles/editor.css +8 -7
- package/src/client/ui/ClaimRouter.ts +1 -1
- package/src/client/ui/EditorView.ts +123 -76
- package/src/client/ui/OfficialSideTab.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.6",
|
|
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": [
|
package/src/client/index.ts
CHANGED
|
@@ -64,6 +64,7 @@ import { disposeSnippets } from './snippets/provider.js'
|
|
|
64
64
|
import { disposeLaunchJson } from './dap/launchSnippetProvider.js'
|
|
65
65
|
import { disposeAiInline } from './ai/inlineProvider.js'
|
|
66
66
|
import { readSessionScope, subscribeScope } from './sessionScope.js'
|
|
67
|
+
import { installPlanBridge } from './planBridge.js'
|
|
67
68
|
import { flushUiState, hydrateUiState, installUiStatePersist, onUiStateReady, reportUiStateEvent, uiStateSettled } from './state/uiStatePersist.js'
|
|
68
69
|
import { workspaceScopeOf } from './state/scopeStore.js'
|
|
69
70
|
import { dapStore } from './dap/store.js'
|
|
@@ -188,6 +189,7 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
188
189
|
const registry: FileOpenerRegistry = createFileOpenerRegistry()
|
|
189
190
|
const workspaces = ctx.get('workspaces')
|
|
190
191
|
const sessions = ctx.get('sessions')
|
|
192
|
+
ctx.effect(() => installPlanBridge(ctx, schedule), 'vscode-mode: plan integration')
|
|
191
193
|
|
|
192
194
|
// 外部深链落地:Windows 右键菜单 / Unity 外部编辑器 → 浏览器 URL 参数 → 打开规则路由
|
|
193
195
|
ctx.effect(() => setupExtOpen(ctx), 'vscode-mode: external open')
|
|
@@ -1,190 +1,250 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
*
|
|
10
|
-
* @
|
|
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
|
|
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
|
-
|
|
80
|
+
let listeners = []
|
|
16
81
|
let lastKey = null
|
|
82
|
+
let generation = 0
|
|
83
|
+
let theme = themeNameOf()
|
|
17
84
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
/**
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|