dsh-vscode-mode 0.4.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -2
- package/lib/client.js +8854 -4023
- package/lib/client.js.map +1 -1
- package/lib/index.js +1842 -65
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/index.ts +3 -0
- package/src/client/monaco/lsp/providers.ts +28 -6
- package/src/client/navHighlight.ts +96 -0
- package/src/client/paths.ts +2 -0
- package/src/client/rpc.ts +32 -12
- package/src/client/sidebar/SidebarView.ts +6 -2
- package/src/client/sidebar/menuItems.ts +93 -2
- package/src/client/sidebar/panels/FileExplorer.ts +18 -1
- package/src/client/sidebar/panels/SvnPanel.ts +303 -0
- package/src/client/sidebar/panels/index.ts +44 -3
- package/src/client/sidebar/types.ts +20 -0
- package/src/client/styles/editor.css +177 -0
- package/src/client/svnActions.ts +153 -0
- package/src/client/svnLog.ts +356 -0
- package/src/client/svnStatus.ts +363 -0
- package/src/client/svnStore.ts +150 -0
- package/src/client/tabMenu.ts +51 -0
- package/src/client/ui/EditorView.ts +583 -24
- package/src/client/ui/LogDialog.ts +333 -0
- package/src/client/ui/McpSettings.ts +98 -0
- package/src/client/ui/ModalShell.ts +79 -0
- package/src/client/ui/SideBySideDiff.tsx +139 -0
- package/src/client/ui/SnippetsPicker.ts +70 -58
- package/src/client/ui/SvnDiffPanel.ts +50 -0
- package/src/client/ui/SvnLogDialog.ts +1141 -0
- package/src/client/ui/SvnLogStats.ts +243 -0
- package/src/client/ui/commandCatalog.ts +72 -0
- package/src/debugLog.ts +81 -12
- package/src/fileOpenSettings.ts +40 -3
- package/src/index.ts +5 -1
- package/src/rpc.ts +41 -4
- package/src/shared/rpc.ts +53 -1
- package/src/shared/svn.ts +377 -0
- package/src/shared/svnActions.ts +296 -0
- package/src/svn.ts +870 -0
- package/src/svnLog.ts +315 -0
- package/src/svnText.ts +175 -0
- package/src/svnXml.ts +110 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — 诊断日志弹窗(居中弹窗形态,ModalShell 外壳)。
|
|
4
|
+
*
|
|
5
|
+
* 形态对齐 SvnLogDialog:单列日志区 + 头部工具栏 + 底部状态行。
|
|
6
|
+
* 数据流:组件自取数据(edrv.dlog.list/read/clear/reveal),EditorView 只负责开与关;
|
|
7
|
+
* 装载/动作集中在 useDlogData(单一数据资源),组件只留视图状态与渲染。
|
|
8
|
+
* 级别过滤与文本搜索为**前端过滤**(日志文件 ≤512KB,整读后过滤,不回传服务端)。
|
|
9
|
+
* 自动刷新:2s 轮询尾部读取(静默,不闪 busy);开关「启用诊断日志」即改即生效
|
|
10
|
+
* (client/rpc.ts 的 setDebugEnabled 写内存缓存 + localStorage,免刷新)。
|
|
11
|
+
* 行格式(host debugLog 落盘):`<ISO时间> [level] 文本`;旧格式无级别标记时 level 为空。
|
|
12
|
+
* 作者 ddj 2026年09月17号
|
|
13
|
+
*/
|
|
14
|
+
import React from 'react'
|
|
15
|
+
import { ModalShell } from './ModalShell.js'
|
|
16
|
+
import { isDebug, rpc, setDebugEnabled } from '../rpc.js'
|
|
17
|
+
|
|
18
|
+
/** 弹窗宽度(日志行长可观,横向铺开;窄屏回落)。 */
|
|
19
|
+
const DIALOG_WIDTH = 'min(880px, calc(100vw - 40px))'
|
|
20
|
+
/** 自动刷新轮询间隔(毫秒)。 */
|
|
21
|
+
const AUTO_REFRESH_MS = 2000
|
|
22
|
+
/** 级别过滤档位(全部 + 四级别,与 LoggerLevel 对齐)。 */
|
|
23
|
+
const LEVEL_OPTIONS = ['all', 'debug', 'info', 'warn', 'error']
|
|
24
|
+
/** 级别 → 展示名(筛选下拉与行标签共用)。 */
|
|
25
|
+
const LEVEL_LABEL = { all: '全部', debug: '调试', info: '信息', warn: '警告', error: '错误' }
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 解析一行日志:`<ISO> [level] text`(旧格式无级别段 → level 为空串)。
|
|
29
|
+
* @author ddj 2026年09月17号
|
|
30
|
+
* @param line 原始行
|
|
31
|
+
* @returns { at, level, text }(at 无法识别时为空串)
|
|
32
|
+
*/
|
|
33
|
+
export function parseLogLine(line) {
|
|
34
|
+
const text = String(line ?? '')
|
|
35
|
+
const matched = /^(\S+) (?:\[(debug|info|warn|error)\] )?([\s\S]*)$/.exec(text)
|
|
36
|
+
if (!matched) return { at: '', level: '', text }
|
|
37
|
+
const [, at, level, body] = matched
|
|
38
|
+
return { at, level: level || '', text: body }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 过滤日志行(级别 + 关键字;无级别旧行只在「全部」档显示)。
|
|
43
|
+
* @author ddj 2026年09月17号
|
|
44
|
+
* @param rows 已解析行数组
|
|
45
|
+
* @param level 档位('all' 或具体级别)
|
|
46
|
+
* @param keyword 关键字(大小写不敏感;空串不过滤)
|
|
47
|
+
* @returns 过滤后行
|
|
48
|
+
*/
|
|
49
|
+
export function filterRows(rows, level, keyword) {
|
|
50
|
+
const key = String(keyword ?? '').trim().toLowerCase()
|
|
51
|
+
return rows.filter((row) => {
|
|
52
|
+
if (level !== 'all' && row.level !== level) return false
|
|
53
|
+
if (!key) return true
|
|
54
|
+
return (row.at + ' ' + row.text).toLowerCase().includes(key)
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 字节数格式化(B / KB / MB,一位小数)。
|
|
60
|
+
* @author ddj 2026年09月17号
|
|
61
|
+
* @param bytes 字节数
|
|
62
|
+
* @returns 展示文本
|
|
63
|
+
*/
|
|
64
|
+
export function formatBytes(bytes) {
|
|
65
|
+
const n = Number(bytes ?? 0)
|
|
66
|
+
if (n < 1024) return n + ' B'
|
|
67
|
+
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB'
|
|
68
|
+
return (n / (1024 * 1024)).toFixed(1) + ' MB'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* RPC 调用简包装:失败/网络异常统一回 null,错误文案取 error 字段。
|
|
73
|
+
* @author ddj 2026年09月17号
|
|
74
|
+
* @param sessionId 会话 id
|
|
75
|
+
* @param method 方法名
|
|
76
|
+
* @param args 参数
|
|
77
|
+
* @returns 成功载荷或 null
|
|
78
|
+
*/
|
|
79
|
+
async function dlogRpc(sessionId, method, args) {
|
|
80
|
+
try {
|
|
81
|
+
const res = await rpc(method, { sessionId, ...args })
|
|
82
|
+
return res.ok ? res : { ok: false, error: res.error }
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return { ok: false, error: String(e) }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 诊断日志数据资源 hook:清单/尾部读取/清空/打开目录 + 自动刷新轮询。
|
|
90
|
+
* 只做数据与装载动作,不含视图状态(级别/关键字/开关在组件侧)。
|
|
91
|
+
* @author ddj 2026年09月17号
|
|
92
|
+
* @param sessionId 会话 id
|
|
93
|
+
* @param auto 是否自动刷新(2s 静默轮询尾部)
|
|
94
|
+
* @returns 数据状态与动作(pick 切换文件;refresh 手动全量刷新)
|
|
95
|
+
*/
|
|
96
|
+
function useDlogData(sessionId, auto) {
|
|
97
|
+
const [files, setFiles] = React.useState(null)
|
|
98
|
+
const [current, setCurrent] = React.useState(null)
|
|
99
|
+
const [picked, setPicked] = React.useState(null)
|
|
100
|
+
const [read, setRead] = React.useState(null)
|
|
101
|
+
const [busy, setBusy] = React.useState(false)
|
|
102
|
+
const [error, setError] = React.useState('')
|
|
103
|
+
const busyRef = React.useRef(false)
|
|
104
|
+
busyRef.current = busy
|
|
105
|
+
|
|
106
|
+
/** 拉清单:首次取回后默认选中「本工作区」那份(不存在则选最近一份)。 */
|
|
107
|
+
const loadList = async () => {
|
|
108
|
+
const res = await dlogRpc(sessionId, 'edrv.dlog.list', {})
|
|
109
|
+
if (!res.ok) { setError(res.error); return }
|
|
110
|
+
setFiles(res.files)
|
|
111
|
+
setCurrent(res.current)
|
|
112
|
+
setPicked((prev) => prev ?? res.current ?? res.files[0]?.name ?? null)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 尾部读取指定文件(silent = 自动刷新静默模式,不闪 busy、不清旧内容)。 */
|
|
116
|
+
const loadRead = async (name, silent) => {
|
|
117
|
+
if (!name) { setRead(null); return }
|
|
118
|
+
if (!silent) setBusy(true)
|
|
119
|
+
const res = await dlogRpc(sessionId, 'edrv.dlog.read', { file: name })
|
|
120
|
+
if (!res.ok) { setError(res.error); if (!silent) setRead(null) }
|
|
121
|
+
else { setError(''); setRead(res) }
|
|
122
|
+
if (!silent) setBusy(false)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 首次进入:清单 + 默认文件;切换文件:重读
|
|
126
|
+
React.useEffect(() => { void loadList() }, [sessionId])
|
|
127
|
+
React.useEffect(() => { void loadRead(picked) }, [picked, sessionId])
|
|
128
|
+
|
|
129
|
+
// 自动刷新:busy 中跳过本轮,避免与手动刷新打架
|
|
130
|
+
React.useEffect(() => {
|
|
131
|
+
if (!auto || !picked) return undefined
|
|
132
|
+
const timer = setInterval(() => { if (!busyRef.current) void loadRead(picked, true) }, AUTO_REFRESH_MS)
|
|
133
|
+
return () => clearInterval(timer)
|
|
134
|
+
}, [auto, picked, sessionId])
|
|
135
|
+
|
|
136
|
+
/** 手动刷新:清单 + 当前文件全量重拉。 */
|
|
137
|
+
const refresh = async () => {
|
|
138
|
+
await loadList()
|
|
139
|
+
await loadRead(picked)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 清空当前日志文件后重读。 */
|
|
143
|
+
const clear = async () => {
|
|
144
|
+
if (!picked) return
|
|
145
|
+
setBusy(true)
|
|
146
|
+
const res = await dlogRpc(sessionId, 'edrv.dlog.clear', { file: picked })
|
|
147
|
+
if (!res.ok) setError(res.error)
|
|
148
|
+
else { setError(''); await loadRead(picked) }
|
|
149
|
+
setBusy(false)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 打开日志根目录(OS 文件管理器)。 */
|
|
153
|
+
const reveal = async () => {
|
|
154
|
+
const res = await dlogRpc(sessionId, 'edrv.dlog.reveal', {})
|
|
155
|
+
if (!res.ok) setError(res.error)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 切换查看的日志文件(清旧内容防闪烁)。 */
|
|
159
|
+
const pick = (name) => { setPicked(name || null); setRead(null) }
|
|
160
|
+
|
|
161
|
+
return { files, current, picked, read, busy, error, pick, refresh, clear, reveal }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 级别 → 配色类名(error 红 / warn 黄 / info 蓝 / debug 灰 / 未知默认)。
|
|
166
|
+
* @author ddj 2026年09月17号
|
|
167
|
+
* @param level 行级别(可空)
|
|
168
|
+
* @returns 类名后缀
|
|
169
|
+
*/
|
|
170
|
+
function toneClassOf(level) {
|
|
171
|
+
return LEVEL_OPTIONS.includes(level) ? level : 'plain'
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 单行日志(时间 + 级别标签 + 文本;完整时间入 title)。
|
|
176
|
+
* @author ddj 2026年09月17号
|
|
177
|
+
* @param props.row 已解析行
|
|
178
|
+
* @returns 行元素
|
|
179
|
+
*/
|
|
180
|
+
function LogRow(props) {
|
|
181
|
+
const { row } = props
|
|
182
|
+
return React.createElement('div', { className: 'edrv-dlog-row edrv-dlog-row-' + toneClassOf(row.level) },
|
|
183
|
+
React.createElement('span', { className: 'edrv-dlog-at', title: row.at }, row.at ? row.at.slice(11, 19) : '--:--:--'),
|
|
184
|
+
React.createElement('span', { className: 'edrv-dlog-lvl' }, row.level ? LEVEL_LABEL[row.level] : '·'),
|
|
185
|
+
React.createElement('span', { className: 'edrv-dlog-text' }, row.text))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 日志主体(滚动区;空态分「未加载 / 暂无日志 / 无匹配」三种)。
|
|
190
|
+
* @author ddj 2026年09月17号
|
|
191
|
+
* @param props.rows 过滤后行(null = 未加载)
|
|
192
|
+
* @param props.busy 读取中
|
|
193
|
+
* @param props.keyword 当前关键字
|
|
194
|
+
* @param props.bodyRef 主体滚动容器 ref(自动滚到底)
|
|
195
|
+
* @returns 主体元素
|
|
196
|
+
*/
|
|
197
|
+
function LogBody(props) {
|
|
198
|
+
const { rows, busy, keyword, bodyRef } = props
|
|
199
|
+
let inner
|
|
200
|
+
if (rows === null) {
|
|
201
|
+
inner = React.createElement('div', { className: 'edrv-tree-loading' }, busy ? '读取日志中…' : '尚未加载')
|
|
202
|
+
} else if (!rows.length) {
|
|
203
|
+
inner = React.createElement('div', { className: 'edrv-tree-loading' },
|
|
204
|
+
keyword ? '无匹配行' : '暂无日志:开启下方「启用诊断日志」并触发一次差异渲染等操作后生成')
|
|
205
|
+
} else {
|
|
206
|
+
inner = rows.map((row, idx) => React.createElement(LogRow, { key: idx, row }))
|
|
207
|
+
}
|
|
208
|
+
return React.createElement('div', { className: 'edrv-dlog-scroll', ref: bodyRef }, inner)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 头部工具栏(标题 + 文件选择 + 级别 + 搜索 + 动作按钮)。
|
|
213
|
+
* @author ddj 2026年09月17号
|
|
214
|
+
* @param props 见内部解构(files/picked/current/level/query/busy/auto 及回调)
|
|
215
|
+
* @returns 头部元素
|
|
216
|
+
*/
|
|
217
|
+
function LogHead(props) {
|
|
218
|
+
const { files, picked, current, level, query, busy, auto, onPick, onLevel, onQuery, onAuto, onRefresh, onCopy, onClear, onReveal, onClose } = props
|
|
219
|
+
const options = (files || []).map((f) => React.createElement('option', {
|
|
220
|
+
key: f.name, value: f.name,
|
|
221
|
+
}, f.name + (f.name === current ? '(本工作区)' : '') + ' · ' + formatBytes(f.bytes)))
|
|
222
|
+
return React.createElement('div', { className: 'edrv-dlog-head' },
|
|
223
|
+
React.createElement('h3', { className: 'edrv-dlog-title' }, '诊断日志'),
|
|
224
|
+
React.createElement('select', {
|
|
225
|
+
className: 'edrv-dlog-file', value: picked ?? '', disabled: !(files && files.length),
|
|
226
|
+
onChange: (event) => onPick(event.target.value),
|
|
227
|
+
}, options.length ? options : React.createElement('option', { value: '' }, '(无日志文件)')),
|
|
228
|
+
React.createElement('select', {
|
|
229
|
+
className: 'edrv-dlog-level', value: level, onChange: (event) => onLevel(event.target.value),
|
|
230
|
+
}, LEVEL_OPTIONS.map((lv) => React.createElement('option', { key: lv, value: lv }, LEVEL_LABEL[lv]))),
|
|
231
|
+
React.createElement('input', {
|
|
232
|
+
className: 'edrv-dlog-search', placeholder: '搜索日志文本', value: query,
|
|
233
|
+
onChange: (event) => onQuery(event.target.value),
|
|
234
|
+
}),
|
|
235
|
+
React.createElement('label', { className: 'edrv-dlog-auto', title: '每 2 秒静默拉取一次尾部日志' },
|
|
236
|
+
React.createElement('input', { type: 'checkbox', checked: auto, onChange: (event) => onAuto(event.target.checked) }),
|
|
237
|
+
'自动刷新'),
|
|
238
|
+
React.createElement('button', { className: 'edrv-svn-act', disabled: busy, title: '重新读取清单与日志', onClick: onRefresh }, busy ? '…' : '⟳'),
|
|
239
|
+
React.createElement('button', { className: 'edrv-svn-act', title: '复制当前过滤后的日志内容', onClick: onCopy }, '复制'),
|
|
240
|
+
React.createElement('button', { className: 'edrv-svn-act edrv-svn-act-danger', title: '清空当前日志文件', onClick: onClear }, '清空'),
|
|
241
|
+
React.createElement('button', { className: 'edrv-svn-act', title: '在文件管理器中打开日志目录', onClick: onReveal }, '目录'),
|
|
242
|
+
React.createElement('button', { className: 'edrv-svn-act', title: '关闭', onClick: onClose }, '✕'))
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 底部状态行(路径 + 统计 + 截头提示 + 诊断开关)。
|
|
247
|
+
* @author ddj 2026年09月17号
|
|
248
|
+
* @param props.read 读取结果(可 null)
|
|
249
|
+
* @param props.total 过滤前总行数
|
|
250
|
+
* @param props.shown 过滤后行数
|
|
251
|
+
* @param props.enabled 诊断开关
|
|
252
|
+
* @param props.copied 复制提示在途
|
|
253
|
+
* @param props.onToggle 开关回调
|
|
254
|
+
* @returns 底部元素
|
|
255
|
+
*/
|
|
256
|
+
function LogFoot(props) {
|
|
257
|
+
const { read, total, shown, enabled, copied, onToggle } = props
|
|
258
|
+
const stat = read
|
|
259
|
+
? '共 ' + total + ' 行' + (shown !== total ? '(过滤后 ' + shown + ' 行)' : '') + ' · ' + formatBytes(read.size) + (read.truncated ? ' · 已截头(仅保留尾部)' : '')
|
|
260
|
+
: '未加载'
|
|
261
|
+
return React.createElement('div', { className: 'edrv-dlog-foot' },
|
|
262
|
+
React.createElement('span', { className: 'edrv-dlog-path', title: read?.path || '' }, read?.path || '—'),
|
|
263
|
+
React.createElement('span', { className: 'edrv-dlog-stat' }, stat + (copied ? ' · 已复制到剪贴板' : '')),
|
|
264
|
+
React.createElement('span', { style: { flex: 1 } }),
|
|
265
|
+
React.createElement('label', { className: 'edrv-dlog-switch', title: '开启后 client 诊断日志上报 host 落盘(立即生效,免刷新)' },
|
|
266
|
+
React.createElement('input', { type: 'checkbox', checked: enabled, onChange: (event) => onToggle(event.target.checked) }),
|
|
267
|
+
'启用诊断日志'))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 诊断日志弹窗主体:数据经 useDlogData,组件只持视图状态与渲染。
|
|
272
|
+
* @author ddj 2026年09月17号
|
|
273
|
+
* @param props.sessionId 会话 id
|
|
274
|
+
* @param props.onClose 关闭回调
|
|
275
|
+
* @returns 弹窗元素
|
|
276
|
+
*/
|
|
277
|
+
export function LogDialog(props) {
|
|
278
|
+
const { sessionId, onClose } = props
|
|
279
|
+
const [level, setLevel] = React.useState('all')
|
|
280
|
+
const [query, setQuery] = React.useState('')
|
|
281
|
+
const [auto, setAuto] = React.useState(false)
|
|
282
|
+
const [enabled, setEnabledState] = React.useState(isDebug())
|
|
283
|
+
const [copied, setCopied] = React.useState(false)
|
|
284
|
+
const bodyRef = React.useRef(null)
|
|
285
|
+
const data = useDlogData(sessionId, auto)
|
|
286
|
+
|
|
287
|
+
// 新内容到底:日志语义「最新在底部」,每次 read 更新后滚到底
|
|
288
|
+
React.useEffect(() => {
|
|
289
|
+
const host = bodyRef.current
|
|
290
|
+
if (host) host.scrollTop = host.scrollHeight
|
|
291
|
+
}, [data.read])
|
|
292
|
+
|
|
293
|
+
// 复制提示自动消退
|
|
294
|
+
React.useEffect(() => {
|
|
295
|
+
if (!copied) return undefined
|
|
296
|
+
const timer = setTimeout(() => setCopied(false), 1500)
|
|
297
|
+
return () => clearTimeout(timer)
|
|
298
|
+
}, [copied])
|
|
299
|
+
|
|
300
|
+
const allRows = React.useMemo(() => {
|
|
301
|
+
if (!data.read?.content) return []
|
|
302
|
+
return data.read.content.split('\n').filter((line) => line.trim()).map(parseLogLine)
|
|
303
|
+
}, [data.read])
|
|
304
|
+
const rows = React.useMemo(() => filterRows(allRows, level, query), [allRows, level, query])
|
|
305
|
+
|
|
306
|
+
/** 复制当前过滤后的日志行(还原级别标记,便于贴给他人排查)。 */
|
|
307
|
+
const copy = async () => {
|
|
308
|
+
if (!rows.length) return
|
|
309
|
+
try {
|
|
310
|
+
await navigator.clipboard.writeText(rows.map((r) => (r.at ? r.at + ' ' : '') + (r.level ? '[' + r.level + '] ' : '') + r.text).join('\n'))
|
|
311
|
+
setCopied(true)
|
|
312
|
+
} catch (e) { /* 剪贴板不可用:状态行已提示复制在途,此处静默 */ }
|
|
313
|
+
}
|
|
314
|
+
const toggleEnabled = (next) => {
|
|
315
|
+
setDebugEnabled(next)
|
|
316
|
+
setEnabledState(next)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const errLine = data.error ? React.createElement('div', { className: 'edrv-dlog-error' }, String(data.error)) : null
|
|
320
|
+
return React.createElement(ModalShell, {
|
|
321
|
+
dialogClass: 'edrv-dlog-dialog',
|
|
322
|
+
width: DIALOG_WIDTH,
|
|
323
|
+
onClose,
|
|
324
|
+
},
|
|
325
|
+
React.createElement(LogHead, {
|
|
326
|
+
files: data.files, picked: data.picked, current: data.current, level, query, busy: data.busy, auto,
|
|
327
|
+
onPick: data.pick, onLevel: setLevel, onQuery: setQuery, onAuto: setAuto,
|
|
328
|
+
onRefresh: data.refresh, onCopy: copy, onClear: data.clear, onReveal: data.reveal, onClose,
|
|
329
|
+
}),
|
|
330
|
+
errLine,
|
|
331
|
+
React.createElement(LogBody, { rows, busy: data.busy, keyword: query.trim(), bodyRef }),
|
|
332
|
+
React.createElement(LogFoot, { read: data.read, total: allRows.length, shown: rows.length, enabled, copied, onToggle: toggleEnabled }))
|
|
333
|
+
}
|
|
@@ -11,6 +11,7 @@ import '../styles/mcp.css'
|
|
|
11
11
|
import { availableOpeners, AUTO_OPEN_TOOL } from '../fileOpeners.js'
|
|
12
12
|
import { SettingsContext } from '../settingsContext.js'
|
|
13
13
|
import { normalizeSidebarMinWidth, SIDEBAR_MIN_DEFAULT } from '../sidebarMin.js'
|
|
14
|
+
import { TORTOISE_DIR_DEFAULT } from '../../shared/svn.js'
|
|
14
15
|
import { KeybindingsPanel } from './KeybindingsPanel.js'
|
|
15
16
|
import { LspSettings } from './LspSettings.js'
|
|
16
17
|
import { PerfSettings } from './PerfSettings.js'
|
|
@@ -221,10 +222,107 @@ function GeneralSettings({ registry }) {
|
|
|
221
222
|
React.createElement('small', null, '180–560 px;拖拽低于该宽度自动隐藏')),
|
|
222
223
|
),
|
|
223
224
|
),
|
|
225
|
+
React.createElement(SvnSettingsSection, null),
|
|
224
226
|
React.createElement(IntegrationSettings, null),
|
|
225
227
|
)
|
|
226
228
|
}
|
|
227
229
|
|
|
230
|
+
/** 检测快照 → 一行状态文案(未检测/未检出/能力可用性)。 */
|
|
231
|
+
function svnStatusText(info) {
|
|
232
|
+
if (!info) return '未检测(打开编辑器后自动检测)'
|
|
233
|
+
if (!info.managed) return '当前工作区不受 SVN 管理(SVN 入口隐藏)'
|
|
234
|
+
const parts = [info.svnCli ? 'svn CLI 可用' : 'svn CLI 不可用(更新不可用)']
|
|
235
|
+
parts.push(info.tortoise ? 'TortoiseSVN 可用' : 'TortoiseSVN 不可用(仅 Windows)')
|
|
236
|
+
return '受 SVN 管理 · ' + parts.join(' · ')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* SVN 设置节:路径覆盖 + 检测状态行。
|
|
241
|
+
* 写入经 SettingsContext(namespace dsh-vscode-mode);状态经 svn.status 直测(force),
|
|
242
|
+
* 并监听 edrv:svn-status 事件跟随编辑器侧重测结果刷新。
|
|
243
|
+
* @author ddj 2026年09月16号
|
|
244
|
+
*/
|
|
245
|
+
function SvnSettingsSection() {
|
|
246
|
+
const settings = React.useContext(SettingsContext)
|
|
247
|
+
const snapshot = settings?.getSnapshot?.()
|
|
248
|
+
const loading = !snapshot || snapshot.status === 'loading'
|
|
249
|
+
const unavailable = snapshot?.status === 'unavailable' || !settings
|
|
250
|
+
const notReady = snapshot?.status !== 'ready'
|
|
251
|
+
const [svnPath, setSvnPath] = React.useState('')
|
|
252
|
+
const [svnDraft, setSvnDraft] = React.useState(null)
|
|
253
|
+
const [tortoisePath, setTortoisePath] = React.useState('')
|
|
254
|
+
const [tortoiseDraft, setTortoiseDraft] = React.useState(null)
|
|
255
|
+
const [svnInfo, setSvnInfo] = React.useState(null)
|
|
256
|
+
const [busy, setBusy] = React.useState(false)
|
|
257
|
+
const [error, setError] = React.useState('')
|
|
258
|
+
React.useEffect(() => {
|
|
259
|
+
const onChange = () => {
|
|
260
|
+
const snap = settings?.getSnapshot?.()
|
|
261
|
+
setSvnPath(typeof snap?.value?.svnPath === 'string' ? snap.value.svnPath : '')
|
|
262
|
+
setTortoisePath(typeof snap?.value?.tortoisePath === 'string' ? snap.value.tortoisePath : '')
|
|
263
|
+
}
|
|
264
|
+
onChange()
|
|
265
|
+
return settings?.subscribe?.(onChange)
|
|
266
|
+
}, [settings])
|
|
267
|
+
const probe = React.useCallback(() => {
|
|
268
|
+
rpc('svn.status', { force: true }).then((res) => {
|
|
269
|
+
if (res.ok) { setSvnInfo(res); setError('') }
|
|
270
|
+
else setSvnInfo(null)
|
|
271
|
+
}).catch(() => setSvnInfo(null))
|
|
272
|
+
}, [])
|
|
273
|
+
React.useEffect(() => { probe() }, [probe])
|
|
274
|
+
React.useEffect(() => {
|
|
275
|
+
const onSvnStatus = () => probe()
|
|
276
|
+
window.addEventListener('edrv:svn-status', onSvnStatus)
|
|
277
|
+
return () => window.removeEventListener('edrv:svn-status', onSvnStatus)
|
|
278
|
+
}, [probe])
|
|
279
|
+
/** 提交路径字段(blur/Enter 共用;成功后强制重测反映新路径)。 */
|
|
280
|
+
const savePath = (key, value, apply) => {
|
|
281
|
+
setBusy(true); setError('')
|
|
282
|
+
if (!settings?.set) { setError('设置服务不可用'); setBusy(false); return }
|
|
283
|
+
settings.set(key, value).then(() => { apply(); probe() }).catch((e) => setError(String(e))).finally(() => setBusy(false))
|
|
284
|
+
}
|
|
285
|
+
const commitSvn = () => {
|
|
286
|
+
if (svnDraft === null) return
|
|
287
|
+
const next = svnDraft.trim()
|
|
288
|
+
setSvnDraft(null)
|
|
289
|
+
savePath('svnPath', next, () => setSvnPath(next))
|
|
290
|
+
}
|
|
291
|
+
const commitTortoise = () => {
|
|
292
|
+
if (tortoiseDraft === null) return
|
|
293
|
+
const next = tortoiseDraft.trim()
|
|
294
|
+
setTortoiseDraft(null)
|
|
295
|
+
savePath('tortoisePath', next, () => setTortoisePath(next))
|
|
296
|
+
}
|
|
297
|
+
const disabled = loading || unavailable || notReady || busy || snapshot?.writable === false
|
|
298
|
+
return React.createElement('section', { className: 'vsm-panel' },
|
|
299
|
+
React.createElement('h3', { className: 'vsm-panel-title' }, 'SVN'),
|
|
300
|
+
React.createElement('div', { className: 'vsm-panel-body' },
|
|
301
|
+
React.createElement('div', { className: 'vsm-general-row' },
|
|
302
|
+
React.createElement('span', null, '检测状态'),
|
|
303
|
+
React.createElement('small', null, svnStatusText(svnInfo)),
|
|
304
|
+
React.createElement('button', { className: 'vsm-primary vsm-small', disabled: busy, onClick: probe, title: '重新检测' }, '重测')),
|
|
305
|
+
error && React.createElement('small', { className: 'vsm-mcp-error' }, error),
|
|
306
|
+
React.createElement('label', { className: 'vsm-general-row' },
|
|
307
|
+
React.createElement('span', null, 'svn 可执行路径'),
|
|
308
|
+
React.createElement('input', {
|
|
309
|
+
value: svnDraft ?? svnPath, placeholder: '空 = 使用 PATH 中的 svn', disabled,
|
|
310
|
+
onChange: (event) => setSvnDraft(event.target.value),
|
|
311
|
+
onBlur: commitSvn,
|
|
312
|
+
onKeyDown: (event) => { if (event.key === 'Enter') commitSvn() },
|
|
313
|
+
})),
|
|
314
|
+
React.createElement('label', { className: 'vsm-general-row' },
|
|
315
|
+
React.createElement('span', null, 'TortoiseSVN 目录'),
|
|
316
|
+
React.createElement('input', {
|
|
317
|
+
value: tortoiseDraft ?? tortoisePath, placeholder: TORTOISE_DIR_DEFAULT, disabled,
|
|
318
|
+
onChange: (event) => setTortoiseDraft(event.target.value),
|
|
319
|
+
onBlur: commitTortoise,
|
|
320
|
+
onKeyDown: (event) => { if (event.key === 'Enter') commitTortoise() },
|
|
321
|
+
})),
|
|
322
|
+
),
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
228
326
|
/** 兼容性小节:host 报告(RPC compat)+ 客户端本地适配项。 */
|
|
229
327
|
function CompatSection({ getSummary }) {
|
|
230
328
|
const [report, setReport] = React.useState(null)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — 通用弹窗外壳(居中卡片 + 遮罩 + Esc/遮罩关闭)。
|
|
4
|
+
*
|
|
5
|
+
* 为什么抽:仓库里已有三处「自建遮罩 + createPortal + Esc 监听」的近似实现
|
|
6
|
+
* (代码片段选择器、工作区选择、以及 P3 日志与 P4 提交弹窗要新增的两处)。
|
|
7
|
+
* 除样板重复外,还有三条硬约定必须处处遵守,靠各写一遍必然漏:
|
|
8
|
+
* - 浮层经 createPortal 挂 body,根节点必须带 `data-edrv-view`(editor.css 的浮层样式
|
|
9
|
+
* 全部以 `[data-edrv-view]` 作用域限定;见 tests/overlayScope.test.ts)
|
|
10
|
+
* - 卡片复用 `.vsm-mcp-dialog`(与「添加 MCP」同一视觉语言)
|
|
11
|
+
* - z-index 必须落在既有阶梯内(见 tests/overlayZOrder.test.ts):命令栏 200 >
|
|
12
|
+
* 片段二级 197 > 片段一级 195 > 命令栏遮罩 190。本外壳默认 190(不遮挡命令栏),
|
|
13
|
+
* 需要更高的调用方显式传 zIndex。
|
|
14
|
+
*
|
|
15
|
+
* 作者 ddj 2026年09月16号
|
|
16
|
+
*/
|
|
17
|
+
import React from 'react'
|
|
18
|
+
import { createPortal } from 'react-dom'
|
|
19
|
+
|
|
20
|
+
/** 弹窗外壳 props。 */
|
|
21
|
+
export interface ModalShellProps {
|
|
22
|
+
/** 卡片自定义类名(附加在 vsm-mcp-dialog 之后)。 */
|
|
23
|
+
dialogClass?: string
|
|
24
|
+
/** 遮罩类名(默认 edrv-modal-mask)。 */
|
|
25
|
+
maskClass?: string
|
|
26
|
+
/** 卡片宽度 CSS(默认 min(560px, calc(100vw - 40px)))。 */
|
|
27
|
+
width?: string
|
|
28
|
+
/** 卡片附加样式(P0-14:可调尺寸弹窗传入受控 height;与 width 合并,缺省不影响既有弹窗)。 */
|
|
29
|
+
cardStyle?: Record<string, string>
|
|
30
|
+
/** 遮罩 z-index(默认 190,落在既有阶梯内)。 */
|
|
31
|
+
zIndex?: number
|
|
32
|
+
/** 点击遮罩是否关闭(默认 true)。 */
|
|
33
|
+
closeOnMask?: boolean
|
|
34
|
+
/** Esc 是否关闭(默认 true)。 */
|
|
35
|
+
closeOnEsc?: boolean
|
|
36
|
+
/** 关闭回调。 */
|
|
37
|
+
onClose?: () => void
|
|
38
|
+
/** 卡片内容。 */
|
|
39
|
+
children?: unknown
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 通用弹窗外壳(居中 + 遮罩 + Esc/遮罩关闭 + portal 到 body)。
|
|
44
|
+
* @author ddj 2026年09月16号
|
|
45
|
+
* @param props 外壳配置
|
|
46
|
+
* @returns portal 元素
|
|
47
|
+
*/
|
|
48
|
+
export function ModalShell(props: ModalShellProps) {
|
|
49
|
+
const {
|
|
50
|
+
dialogClass, maskClass, width, cardStyle, zIndex = 190, closeOnMask = true, closeOnEsc = true, onClose, children,
|
|
51
|
+
} = props || {}
|
|
52
|
+
|
|
53
|
+
React.useEffect(() => {
|
|
54
|
+
if (!closeOnEsc) return undefined
|
|
55
|
+
const onKey = (event) => {
|
|
56
|
+
if (event.key !== 'Escape') return
|
|
57
|
+
// 阻止冒泡:避免编辑器/对话输入框同时响应 Esc
|
|
58
|
+
event.stopPropagation()
|
|
59
|
+
onClose?.()
|
|
60
|
+
}
|
|
61
|
+
window.addEventListener('keydown', onKey, true)
|
|
62
|
+
return () => window.removeEventListener('keydown', onKey, true)
|
|
63
|
+
}, [closeOnEsc, onClose])
|
|
64
|
+
|
|
65
|
+
const mask = React.createElement('div', {
|
|
66
|
+
className: maskClass || 'edrv-modal-mask',
|
|
67
|
+
style: zIndex ? { zIndex } : undefined,
|
|
68
|
+
onClick: () => { if (closeOnMask) onClose?.() },
|
|
69
|
+
},
|
|
70
|
+
React.createElement('div', {
|
|
71
|
+
className: 'vsm-mcp-dialog' + (dialogClass ? ' ' + dialogClass : ''),
|
|
72
|
+
style: width || cardStyle ? Object.assign({}, width ? { width } : null, cardStyle || null) : undefined,
|
|
73
|
+
// 卡片内点击不冒泡到遮罩(否则点内容就关闭)
|
|
74
|
+
onClick: (event) => event.stopPropagation(),
|
|
75
|
+
}, children))
|
|
76
|
+
|
|
77
|
+
// 根节点带 data-edrv-view:editor.css 的浮层样式均以此为作用域前缀
|
|
78
|
+
return createPortal(React.createElement('div', { 'data-edrv-view': '1' }, mask), document.body)
|
|
79
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — 只读并排差异视图(Monaco createDiffEditor 的通用封装)。
|
|
4
|
+
*
|
|
5
|
+
* 三处共用同一实现,避免各自处理 Monaco 的 model 生命周期与已知坑:
|
|
6
|
+
* - P2 基线差异(BASE ↔ 工作区)
|
|
7
|
+
* - P3 版本差异(REV-1 ↔ REV)
|
|
8
|
+
* - P4 提交前预览(工作区 ↔ 即将提交内容,届时按需扩展)
|
|
9
|
+
*
|
|
10
|
+
* 关键实现要点(来自 P2 实测):
|
|
11
|
+
* - 复用既有 Monaco 单例(与编辑器同实例,主题跟随官方令牌),不占编辑器自身 model
|
|
12
|
+
* - URI 必须先去前导 `/`:`'scheme:///' + '/abs'` 会拼出四斜杠 → Uri.parse 抛错 → 白屏
|
|
13
|
+
* - 只销毁本视图创建的 model,复用到的留给下次打开(避免闪烁/重建)
|
|
14
|
+
* - 左侧为 null 时不建编辑器,只显示说明(新增文件的正常语义,不是错误)
|
|
15
|
+
*
|
|
16
|
+
* 作者 ddj 2026年09月16号
|
|
17
|
+
*/
|
|
18
|
+
import React from 'react'
|
|
19
|
+
import { langOf } from '../monaco/loader.js'
|
|
20
|
+
import { themeNameOf } from '../monaco/theme.js'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 构造 model URI(对齐 EditorView.getModel 的 edrv URI 约定;路径剥前导斜杠防四斜杠)。
|
|
24
|
+
* @author ddj 2026年09月16号
|
|
25
|
+
* @param scheme URI scheme
|
|
26
|
+
* @param path 文件路径
|
|
27
|
+
* @param side 侧别(同文件不同侧需不同 scheme,避免 model 冲突)
|
|
28
|
+
* @returns Monaco Uri
|
|
29
|
+
*/
|
|
30
|
+
function uriOf(scheme, path, side) {
|
|
31
|
+
return window.monaco.Uri.parse(scheme + '-' + side + ':///' + encodeURI(String(path ?? '').replace(/^\/+/, '')))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 取(或建)指定 URI 的只读 model:命中则更新内容复用,未命中则创建。
|
|
36
|
+
* @author ddj 2026年09月16号
|
|
37
|
+
* @param monaco Monaco 实例
|
|
38
|
+
* @param uri 目标 URI
|
|
39
|
+
* @param text 内容
|
|
40
|
+
* @param language Monaco language id
|
|
41
|
+
* @param created 本视图创建的 model 收集器(卸载时只销毁自己建的)
|
|
42
|
+
* @returns model
|
|
43
|
+
*/
|
|
44
|
+
function modelFor(monaco, uri, text, language, created) {
|
|
45
|
+
const existing = monaco.editor.getModel(uri)
|
|
46
|
+
if (existing) {
|
|
47
|
+
existing.setValue(text)
|
|
48
|
+
return existing
|
|
49
|
+
}
|
|
50
|
+
const model = monaco.editor.createModel(text, language, uri)
|
|
51
|
+
created.push(model)
|
|
52
|
+
return model
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 挂载只读并排差异编辑器;返回卸载函数。
|
|
57
|
+
* @author ddj 2026年09月16号
|
|
58
|
+
* @param props.host 宿主 DOM 节点
|
|
59
|
+
* @param props.monaco Monaco 实例
|
|
60
|
+
* @param props.path 文件路径(决定语法高亮与 model URI)
|
|
61
|
+
* @param props.left 左侧内容(null 表示该侧无内容)
|
|
62
|
+
* @param props.right 右侧内容
|
|
63
|
+
* @param props.scheme model URI scheme 前缀(区分不同来源,避免跨视图复用串内容)
|
|
64
|
+
* @returns 卸载函数
|
|
65
|
+
*/
|
|
66
|
+
export function mountSideBySide(props) {
|
|
67
|
+
const { host, monaco, path, left, right, scheme = 'edrv-diff' } = props
|
|
68
|
+
const created = []
|
|
69
|
+
const language = langOf(path)
|
|
70
|
+
const leftModel = modelFor(monaco, uriOf(scheme, path, 'left'), left ?? '', language, created)
|
|
71
|
+
const rightModel = modelFor(monaco, uriOf(scheme, path, 'right'), right ?? '', language, created)
|
|
72
|
+
const diff = monaco.editor.createDiffEditor(host, {
|
|
73
|
+
theme: themeNameOf(),
|
|
74
|
+
readOnly: true,
|
|
75
|
+
originalEditable: false,
|
|
76
|
+
renderSideBySide: true,
|
|
77
|
+
// 只读审阅:不要 minimap/字形栏等编辑态附属物
|
|
78
|
+
minimap: { enabled: false },
|
|
79
|
+
glyphMargin: false,
|
|
80
|
+
lineDecorationsWidth: 8,
|
|
81
|
+
scrollBeyondLastLine: false,
|
|
82
|
+
automaticLayout: true,
|
|
83
|
+
renderOverviewRuler: false,
|
|
84
|
+
wordWrap: 'off',
|
|
85
|
+
fontSize: 13,
|
|
86
|
+
lineHeight: 20,
|
|
87
|
+
padding: { top: 6 },
|
|
88
|
+
})
|
|
89
|
+
diff.setModel({ original: leftModel, modified: rightModel })
|
|
90
|
+
return () => {
|
|
91
|
+
try { diff.dispose() } catch (error) { /* 已随宿主卸载 */ }
|
|
92
|
+
for (const model of created) {
|
|
93
|
+
try { model.dispose() } catch (error) { /* 已被其他持有者销毁 */ }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 只读并排差异视图(编辑器主体内的一个分支,非弹窗)。
|
|
100
|
+
* @author ddj 2026年09月16号
|
|
101
|
+
* @param props.monaco Monaco 实例(未就绪为 null,显示加载态)
|
|
102
|
+
* @param props.path 文件路径
|
|
103
|
+
* @param props.left 左侧内容(null = 无内容)
|
|
104
|
+
* @param props.right 右侧内容
|
|
105
|
+
* @param props.title 标题(通常为路径)
|
|
106
|
+
* @param props.leftLabel 左侧标签(如「BASE」「r12」)
|
|
107
|
+
* @param props.rightLabel 右侧标签(如「工作区」「r13」)
|
|
108
|
+
* @param props.emptyNote 左侧无内容时的说明
|
|
109
|
+
* @param props.message 补充说明
|
|
110
|
+
* @param props.scheme model URI scheme 前缀
|
|
111
|
+
* @param props.onClose 关闭回调
|
|
112
|
+
* @returns 差异视图元素
|
|
113
|
+
*/
|
|
114
|
+
export function SideBySideDiff(props) {
|
|
115
|
+
const {
|
|
116
|
+
monaco, path, left, right, title, leftLabel, rightLabel, emptyNote, message, scheme, onClose,
|
|
117
|
+
} = props
|
|
118
|
+
const hostRef = React.useRef(null)
|
|
119
|
+
|
|
120
|
+
React.useEffect(() => {
|
|
121
|
+
const host = hostRef.current
|
|
122
|
+
if (!monaco || !host) return undefined
|
|
123
|
+
return mountSideBySide({ host, monaco, path, left, right, scheme })
|
|
124
|
+
}, [monaco, path, left, right, scheme])
|
|
125
|
+
|
|
126
|
+
const noLeft = left === null || left === undefined
|
|
127
|
+
const noRight = right === null || right === undefined
|
|
128
|
+
|
|
129
|
+
return React.createElement('div', { className: 'edrv-svndiff' },
|
|
130
|
+
React.createElement('div', { className: 'edrv-svndiff-bar' },
|
|
131
|
+
React.createElement('span', { className: 'edrv-svndiff-title', title: title ?? path }, String(title ?? path ?? '')),
|
|
132
|
+
React.createElement('span', { className: 'edrv-svndiff-tag' }, (leftLabel || '左') + ' ↔ ' + (rightLabel || '右')),
|
|
133
|
+
(message ? React.createElement('span', { className: 'edrv-svndiff-tag' }, String(message)) : null),
|
|
134
|
+
React.createElement('span', { style: { flex: 1 } }),
|
|
135
|
+
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '关闭差异视图', onClick: onClose }, '✕ 关闭')),
|
|
136
|
+
((noLeft && noRight) || !monaco
|
|
137
|
+
? React.createElement('div', { className: 'edrv-svndiff-empty' }, emptyNote || '无可比较内容')
|
|
138
|
+
: React.createElement('div', { ref: hostRef, className: 'edrv-svndiff-host' })))
|
|
139
|
+
}
|