dsh-vscode-mode 0.1.22 → 0.1.25
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 +34 -25
- package/lib/client.js +538 -98
- package/lib/client.js.map +1 -1
- package/lib/index.js +4 -1
- package/lib/index.js.map +1 -1
- package/package.json +5 -2
- package/src/client/events.ts +8 -4
- package/src/client/index.ts +36 -9
- package/src/client/monaco/diffRender.ts +3 -0
- package/src/client/sidebar/panels/FileExplorer.ts +12 -2
- package/src/client/sidebarBridge.ts +247 -0
- package/src/client/styles/editor.css +11 -0
- package/src/client/ui/ContextMenu.ts +37 -12
- package/src/client/ui/DiffBadge.ts +5 -3
- package/src/client/ui/EditorView.ts +108 -43
- package/src/client/ui/SideEditorTab.ts +48 -0
- package/src/client/ui/menuPosition.ts +63 -0
- package/src/rpc.ts +7 -1
|
@@ -19,17 +19,22 @@ import { clearDiffDock, publishDiffDock } from '../diffDockStore.js'
|
|
|
19
19
|
import { displayDiffTotal, editorDockMode } from '../diffDock.js'
|
|
20
20
|
import { editorHeight } from '../editorLayout.js'
|
|
21
21
|
import { revealInExplorer as revealPathInExplorer } from '../fileReveal.js'
|
|
22
|
+
import { setSidePending, SIDEBAR_INSTALL_CMD } from '../sidebarBridge.js'
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* 中央编辑区:文件页签(脏点/关闭/打开路径)+ Ctrl+P 搜索 + Monaco 编辑器 +
|
|
25
26
|
* 底部差异条(DiffBox/空态)+ 全局差异下拉(DiffLauncher)+ 状态栏。
|
|
26
27
|
* @param props.sessionId 会话 id
|
|
27
28
|
* @param props.schedule 延时调度(ctx.timeout)
|
|
29
|
+
* @param props.layout 布局形态:'tab'(中央页签,默认)| 'side'(侧边栏面板)
|
|
30
|
+
* @param props.sideHint 旧页签形态下显示侧边栏引导(安装命令)
|
|
28
31
|
*/
|
|
29
32
|
export function EditorView(props) {
|
|
30
33
|
const sessionId = props?.sessionId
|
|
31
34
|
const schedule = props.schedule
|
|
32
35
|
const addToConversation = props.addToConversation
|
|
36
|
+
const layout = props?.layout === 'side' ? 'side' : 'tab'
|
|
37
|
+
const sideHint = props?.sideHint
|
|
33
38
|
const [monaco, setMonaco] = React.useState(null)
|
|
34
39
|
const [monacoErr, setMonacoErr] = React.useState(null)
|
|
35
40
|
const [records, setRecords] = React.useState({})
|
|
@@ -51,10 +56,13 @@ export function EditorView(props) {
|
|
|
51
56
|
const [diffIdx, setDiffIdx] = React.useState(0) // 当前文件内差异位置(x/x 显示)
|
|
52
57
|
const [fileIdx, setFileIdx] = React.useState(0) // 全局差异文件位置(x/x 文件 显示)
|
|
53
58
|
const [tabMenu, setTabMenu] = React.useState(null) // Tab 右键菜单 { x,y,path }(编辑区右键走 Monaco 原生菜单,无此浮层)
|
|
54
|
-
const [sidebarOn, setSidebarOn] = React.useState(
|
|
59
|
+
const [sidebarOn, setSidebarOn] = React.useState(layout !== 'side') // 侧边栏显隐(侧栏形态默认收起)
|
|
55
60
|
const [sidebarW, setSidebarW] = React.useState(240) // 侧边栏宽度
|
|
56
61
|
const [activePanel, setActivePanel] = React.useState('explorer') // 激活面板 id
|
|
57
62
|
const [focusRequest, setFocusRequest] = React.useState(0) // 外部差异聚焦请求版本
|
|
63
|
+
const [hintDismissed, setHintDismissed] = React.useState(() => {
|
|
64
|
+
try { return localStorage.getItem('edrv.side-hint-dismissed') === '1' } catch { return false }
|
|
65
|
+
}) // 侧边栏引导条是否已关闭
|
|
58
66
|
const editorRef = React.useRef(null)
|
|
59
67
|
const viewRootRef = React.useRef(null)
|
|
60
68
|
const monacoRef = React.useRef(null)
|
|
@@ -75,6 +83,8 @@ export function EditorView(props) {
|
|
|
75
83
|
const batchBusyRef = React.useRef(false) // 批量 Keep All/Undo All 防重入
|
|
76
84
|
const menuHandlersRef = React.useRef(null) // 右键菜单动作的最新闭包(Monaco addAction 空依赖回调读取)
|
|
77
85
|
const diffRendererRef = React.useRef(null)
|
|
86
|
+
const layoutRef = React.useRef(layout) // ensureEditor 空依赖闭包读取的稳定布局
|
|
87
|
+
layoutRef.current = layout
|
|
78
88
|
if (!diffRendererRef.current) diffRendererRef.current = createDiffRenderer((sid, t) => dbg(sid, t))
|
|
79
89
|
|
|
80
90
|
const currentRecords = React.useMemo(() => {
|
|
@@ -121,6 +131,25 @@ export function EditorView(props) {
|
|
|
121
131
|
})
|
|
122
132
|
}
|
|
123
133
|
|
|
134
|
+
/**
|
|
135
|
+
* 轮询去重:前后记录内容一致时返回原引用,React 跳过重渲染,
|
|
136
|
+
* 阻断 5s 轮询 → memo 链 → diff 重渲染/日志 的空转。
|
|
137
|
+
* @author ddj 2026年08月26号
|
|
138
|
+
* @param prev 当前 records
|
|
139
|
+
* @param next 轮询新结果
|
|
140
|
+
* @returns 内容一致返回 true
|
|
141
|
+
*/
|
|
142
|
+
const sameRecords = (prev, next) => {
|
|
143
|
+
const pk = Object.keys(prev)
|
|
144
|
+
const nk = Object.keys(next)
|
|
145
|
+
if (pk.length !== nk.length) return false
|
|
146
|
+
for (const k of pk) {
|
|
147
|
+
if (!(k in next)) return false
|
|
148
|
+
if (JSON.stringify(prev[k]) !== JSON.stringify(next[k])) return false
|
|
149
|
+
}
|
|
150
|
+
return true
|
|
151
|
+
}
|
|
152
|
+
|
|
124
153
|
const refreshRecords = (skipStale) => {
|
|
125
154
|
if (!sessionId) return
|
|
126
155
|
rpc('edrv.list', { sessionId, ...(skipStale ? { skipStale: true } : {}) }).then((res) => {
|
|
@@ -130,7 +159,7 @@ export function EditorView(props) {
|
|
|
130
159
|
if (r.archived === true) continue
|
|
131
160
|
map[r.callId] = r
|
|
132
161
|
}
|
|
133
|
-
setRecords(map)
|
|
162
|
+
setRecords((prev) => (sameRecords(prev, map) ? prev : map))
|
|
134
163
|
}).catch((e) => setError('list异常:' + String(e)))
|
|
135
164
|
}
|
|
136
165
|
|
|
@@ -195,10 +224,15 @@ export function EditorView(props) {
|
|
|
195
224
|
|
|
196
225
|
// 对话输入区的唯一差异 dock 读取此会话快照;编辑器卸载时只清理自己的发布者令牌。
|
|
197
226
|
const dockSourceRef = React.useRef({})
|
|
198
|
-
React.useEffect(() => () =>
|
|
227
|
+
React.useEffect(() => () => {
|
|
228
|
+
clearDiffDock(sessionId, dockSourceRef.current)
|
|
229
|
+
if (layout === 'side') setSidePending(sessionId, 0)
|
|
230
|
+
}, [sessionId])
|
|
199
231
|
|
|
200
232
|
// DSH composer 保持原生布局;编辑区只读取几何边界并同步自己的高度。
|
|
233
|
+
// 侧栏形态:面板容器自带高度,跳过 composer 几何同步(面板内无会话滚动区)。
|
|
201
234
|
React.useLayoutEffect(() => {
|
|
235
|
+
if (layout === 'side') return
|
|
202
236
|
const root = viewRootRef.current
|
|
203
237
|
const scroll = root?.closest?.('[data-conversation-scroll]')
|
|
204
238
|
if (!root || !scroll) return
|
|
@@ -249,7 +283,7 @@ export function EditorView(props) {
|
|
|
249
283
|
window.removeEventListener('resize', scheduleUpdate)
|
|
250
284
|
root.style.removeProperty('--edrv-editor-height')
|
|
251
285
|
}
|
|
252
|
-
}, [sessionId])
|
|
286
|
+
}, [sessionId, layout])
|
|
253
287
|
|
|
254
288
|
// localStorage v2 恢复页签
|
|
255
289
|
React.useEffect(() => {
|
|
@@ -273,11 +307,12 @@ export function EditorView(props) {
|
|
|
273
307
|
catch (e) { /* 忽略 */ }
|
|
274
308
|
}, [tabs, active, sessionId])
|
|
275
309
|
|
|
276
|
-
//
|
|
310
|
+
// 侧边栏状态:恢复(显隐/宽度/激活面板);侧栏形态独立键(默认收起,不共享页签形态偏好)
|
|
311
|
+
const sidebarKey = 'edrv.sidebar.' + (layout === 'side' ? 'side.' : '') + String(sessionId)
|
|
277
312
|
React.useEffect(() => {
|
|
278
313
|
if (!sessionId) return
|
|
279
314
|
try {
|
|
280
|
-
const raw = localStorage.getItem(
|
|
315
|
+
const raw = localStorage.getItem(sidebarKey)
|
|
281
316
|
if (raw) {
|
|
282
317
|
const saved = JSON.parse(raw)
|
|
283
318
|
if (typeof saved.on === 'boolean') setSidebarOn(saved.on)
|
|
@@ -285,14 +320,14 @@ export function EditorView(props) {
|
|
|
285
320
|
if (typeof saved.panel === 'string') setActivePanel(saved.panel)
|
|
286
321
|
}
|
|
287
322
|
} catch (e) { /* 损坏忽略 */ }
|
|
288
|
-
}, [sessionId])
|
|
323
|
+
}, [sessionId, sidebarKey])
|
|
289
324
|
|
|
290
325
|
// 侧边栏状态持久化
|
|
291
326
|
React.useEffect(() => {
|
|
292
327
|
if (!sessionId) return
|
|
293
|
-
try { localStorage.setItem(
|
|
328
|
+
try { localStorage.setItem(sidebarKey, JSON.stringify({ on: sidebarOn, width: sidebarW, panel: activePanel })) }
|
|
294
329
|
catch (e) { /* 忽略 */ }
|
|
295
|
-
}, [sidebarOn, sidebarW, activePanel, sessionId])
|
|
330
|
+
}, [sidebarOn, sidebarW, activePanel, sessionId, sidebarKey])
|
|
296
331
|
|
|
297
332
|
// Ctrl+B 切换侧边栏(capture 抢占,避免与 DSH 全局冲突)
|
|
298
333
|
React.useEffect(() => {
|
|
@@ -431,6 +466,7 @@ export function EditorView(props) {
|
|
|
431
466
|
}
|
|
432
467
|
if (editorRef.current || !monacoRef.current) return
|
|
433
468
|
const m = monacoRef.current
|
|
469
|
+
const side = layoutRef.current === 'side'
|
|
434
470
|
const ed = m.editor.create(node, {
|
|
435
471
|
value: '',
|
|
436
472
|
language: 'plaintext',
|
|
@@ -438,7 +474,7 @@ export function EditorView(props) {
|
|
|
438
474
|
fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)',
|
|
439
475
|
fontSize: 13,
|
|
440
476
|
lineHeight: 20,
|
|
441
|
-
minimap: { enabled:
|
|
477
|
+
minimap: { enabled: !side, scale: 1 },
|
|
442
478
|
glyphMargin: true,
|
|
443
479
|
lineDecorationsWidth: 16,
|
|
444
480
|
automaticLayout: true,
|
|
@@ -448,7 +484,7 @@ export function EditorView(props) {
|
|
|
448
484
|
renderWhitespace: 'selection',
|
|
449
485
|
smoothScrolling: true,
|
|
450
486
|
cursorBlinking: 'smooth',
|
|
451
|
-
padding: { top: 8 },
|
|
487
|
+
padding: { top: side ? 6 : 8 },
|
|
452
488
|
})
|
|
453
489
|
ed.addCommand(m.KeyMod.CtrlCmd | m.KeyCode.KeyS, () => { flushSave(); doSave(false) })
|
|
454
490
|
ed.onDidChangeModelContent(() => {
|
|
@@ -901,36 +937,41 @@ export function EditorView(props) {
|
|
|
901
937
|
React.createElement('button', { className: 'edrv-pill edrv-pill-undo', onClick: () => { dismissHover(); actHunk(hoverAct.region, true) } }, '↩ Undo'))
|
|
902
938
|
: null
|
|
903
939
|
|
|
940
|
+
/** 组装差异 dock 快照(发布给 conversation.input.dock 的唯一 DiffBox 实例)。 */
|
|
941
|
+
const buildDockSnapshot = () => ({
|
|
942
|
+
mode: editorDockMode(active),
|
|
943
|
+
pendingRegions,
|
|
944
|
+
staleRegions,
|
|
945
|
+
onAct: actHunk,
|
|
946
|
+
onAcceptFile: acceptFile,
|
|
947
|
+
onUndoFile: undoFile,
|
|
948
|
+
onAcceptAllFiles: acceptAllFiles,
|
|
949
|
+
onUndoAllFiles: undoAllFiles,
|
|
950
|
+
allPendingCount: allPending.length,
|
|
951
|
+
onRollback: rollbackFile,
|
|
952
|
+
onJump: jumpTo,
|
|
953
|
+
otherFiles,
|
|
954
|
+
onOpenOther: (path) => openFile(path, true),
|
|
955
|
+
onOpenLauncher: (tab) => { setLauncherTab(tab || 'pending'); setLauncherOpen(true) },
|
|
956
|
+
onRefresh: reloadFile,
|
|
957
|
+
activePath: active,
|
|
958
|
+
diffIdx: contentReady ? diffIdx : 0,
|
|
959
|
+
diffTotal: displayDiffTotal(contentReady, pendingRegions.length, sum.files.find((file) => file.path === active)?.pending ?? 0),
|
|
960
|
+
fileIdx,
|
|
961
|
+
fileTotal: sum.pendingFiles.length,
|
|
962
|
+
onPrevDiff: () => gotoDiff(-1),
|
|
963
|
+
onNextDiff: () => gotoDiff(1),
|
|
964
|
+
onPrevFile: () => gotoFile(-1),
|
|
965
|
+
onNextFile: () => gotoFile(1),
|
|
966
|
+
onOpenNextFile: openNextFile,
|
|
967
|
+
})
|
|
968
|
+
|
|
904
969
|
React.useEffect(() => {
|
|
905
970
|
if (!sessionId) return
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
staleRegions,
|
|
911
|
-
onAct: actHunk,
|
|
912
|
-
onAcceptFile: acceptFile,
|
|
913
|
-
onUndoFile: undoFile,
|
|
914
|
-
onAcceptAllFiles: acceptAllFiles,
|
|
915
|
-
onUndoAllFiles: undoAllFiles,
|
|
916
|
-
allPendingCount: allPending.length,
|
|
917
|
-
onRollback: rollbackFile,
|
|
918
|
-
onJump: jumpTo,
|
|
919
|
-
otherFiles,
|
|
920
|
-
onOpenOther: (path) => openFile(path, true),
|
|
921
|
-
onOpenLauncher: (tab) => { setLauncherTab(tab || 'pending'); setLauncherOpen(true) },
|
|
922
|
-
onRefresh: reloadFile,
|
|
923
|
-
activePath: active,
|
|
924
|
-
diffIdx: contentReady ? diffIdx : 0,
|
|
925
|
-
diffTotal: displayDiffTotal(contentReady, pendingRegions.length, sum.files.find((file) => file.path === active)?.pending ?? 0),
|
|
926
|
-
fileIdx,
|
|
927
|
-
fileTotal: sum.pendingFiles.length,
|
|
928
|
-
onPrevDiff: () => gotoDiff(-1),
|
|
929
|
-
onNextDiff: () => gotoDiff(1),
|
|
930
|
-
onPrevFile: () => gotoFile(-1),
|
|
931
|
-
onNextFile: () => gotoFile(1),
|
|
932
|
-
onOpenNextFile: openNextFile,
|
|
933
|
-
}, dockSourceRef.current)
|
|
971
|
+
publishDiffDock(sessionId, buildDockSnapshot(), dockSourceRef.current)
|
|
972
|
+
// 侧栏形态同步 Tab 角标计数(仅本形态写入;页签形态清零避免残留)
|
|
973
|
+
if (layout === 'side') setSidePending(sessionId, sum.totalFiles)
|
|
974
|
+
else setSidePending(sessionId, 0)
|
|
934
975
|
})
|
|
935
976
|
|
|
936
977
|
const overlay = launcherOpen
|
|
@@ -982,11 +1023,28 @@ export function EditorView(props) {
|
|
|
982
1023
|
body,
|
|
983
1024
|
hoverEl,
|
|
984
1025
|
overlay)
|
|
1026
|
+
// 侧边栏引导条(仅旧页签形态且未装 betterSidebar 时显示;可复制安装命令、可关闭)
|
|
1027
|
+
const dismissHint = () => {
|
|
1028
|
+
setHintDismissed(true)
|
|
1029
|
+
try { localStorage.setItem('edrv.side-hint-dismissed', '1') } catch (e) { /* 忽略 */ }
|
|
1030
|
+
}
|
|
1031
|
+
const copyInstallCmd = () => {
|
|
1032
|
+
if (navigator.clipboard?.writeText) navigator.clipboard.writeText(SIDEBAR_INSTALL_CMD).catch(() => {})
|
|
1033
|
+
}
|
|
1034
|
+
const sideHintEl = (layout === 'tab' && sideHint && !hintDismissed)
|
|
1035
|
+
? React.createElement('div', { className: 'edrv-side-hint' },
|
|
1036
|
+
React.createElement('span', { className: 'edrv-side-hint-text' }, '安装 dsh-better-sidebar 后可启用侧边栏编辑(对话+编辑同屏):'),
|
|
1037
|
+
React.createElement('code', { className: 'edrv-side-hint-cmd' }, SIDEBAR_INSTALL_CMD),
|
|
1038
|
+
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '复制安装命令', onClick: copyInstallCmd }, '复制命令'),
|
|
1039
|
+
React.createElement('button', { className: 'edrv-side-hint-close', title: '关闭提示', 'aria-label': '关闭提示', onClick: dismissHint }, '×'))
|
|
1040
|
+
: null
|
|
985
1041
|
const mainCol = React.createElement('div', { className: 'edrv-main-col', style: { flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' } },
|
|
986
1042
|
pathBar,
|
|
987
1043
|
tabRow,
|
|
1044
|
+
sideHintEl,
|
|
988
1045
|
editorArea)
|
|
989
1046
|
// 编辑器根节点按 composer 顶部边界动态限高,底部对话区域继续由 DSH 原生渲染。
|
|
1047
|
+
// 侧栏形态由面板容器给高(100%),不做 composer 几何同步。
|
|
990
1048
|
const editorRow = React.createElement('div', { className: 'edrv-editor-row' },
|
|
991
1049
|
(sidebarOn && sidebarPanels
|
|
992
1050
|
? React.createElement(SidebarView, {
|
|
@@ -1001,8 +1059,15 @@ export function EditorView(props) {
|
|
|
1001
1059
|
: null),
|
|
1002
1060
|
mainCol)
|
|
1003
1061
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1062
|
+
const baseStyle = { minHeight: 0, display: 'flex', flexDirection: 'column', background: 'var(--dsw-alias-bg-base,transparent)', overflow: 'hidden' }
|
|
1063
|
+
const rootEl = layout === 'side'
|
|
1064
|
+
? React.createElement('div', { ref: viewRootRef, 'data-edrv-view': '1', 'data-edrv-layout': 'side', className: 'edrv-view-side', style: Object.assign({}, baseStyle, { height: '100%' }) },
|
|
1065
|
+
editorRow,
|
|
1066
|
+
menuBackdrop,
|
|
1067
|
+
tabMenuEl)
|
|
1068
|
+
: React.createElement('div', { ref: viewRootRef, 'data-edrv-view': '1', style: Object.assign({}, baseStyle, { height: 'var(--edrv-editor-height, 100%)', maxHeight: 'var(--edrv-editor-height, 100%)' }) },
|
|
1069
|
+
editorRow,
|
|
1070
|
+
menuBackdrop,
|
|
1071
|
+
tabMenuEl)
|
|
1072
|
+
return rootEl
|
|
1008
1073
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — SideEditorTab:betterSidebar「文件编辑」Tab 的组件包装。
|
|
4
|
+
* 把 TabComponentProps(scope/tab/visible)映射为 EditorView 侧栏形态 props,
|
|
5
|
+
* 并按会话 key 保证切会话状态干净重建;创建种子的初始打开在挂载后投递一次。
|
|
6
|
+
* 作者 ddj 2026年08月25号
|
|
7
|
+
*/
|
|
8
|
+
import React from 'react'
|
|
9
|
+
import { EditorView } from './EditorView.js'
|
|
10
|
+
import { resolveInitialOpen, setSideEditorMounted, takePendingSideOpen } from '../sidebarBridge.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 侧边栏 Tab 组件(TabDescriptor.component 装配)。
|
|
14
|
+
* @param props TabComponentProps(scope.sessionId、tab.path/meta 等)
|
|
15
|
+
* @returns EditorView 侧栏形态
|
|
16
|
+
*/
|
|
17
|
+
export function SideEditorTab(props) {
|
|
18
|
+
const sessionId = props?.scope?.sessionId
|
|
19
|
+
const initial = React.useMemo(() => resolveInitialOpen(props?.tab), [props?.tab])
|
|
20
|
+
const dispatchedRef = React.useRef(false)
|
|
21
|
+
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
setSideEditorMounted(true)
|
|
24
|
+
return () => setSideEditorMounted(false)
|
|
25
|
+
}, [])
|
|
26
|
+
|
|
27
|
+
React.useEffect(() => {
|
|
28
|
+
if (dispatchedRef.current) return
|
|
29
|
+
dispatchedRef.current = true
|
|
30
|
+
// 待消费打开(挂载前暂存的聚焦请求)优先于创建种子;两者都走同事件,EditorView 幂等处理
|
|
31
|
+
const pending = takePendingSideOpen(sessionId)
|
|
32
|
+
const request = pending ?? initial
|
|
33
|
+
window.dispatchEvent(new CustomEvent('edrv:open-editor', {
|
|
34
|
+
detail: { path: request?.path ?? null, focusDiff: request?.focusDiff === true },
|
|
35
|
+
}))
|
|
36
|
+
}, [initial.path, initial.focusDiff, sessionId])
|
|
37
|
+
|
|
38
|
+
return React.createElement(EditorView, {
|
|
39
|
+
key: sessionId,
|
|
40
|
+
sessionId,
|
|
41
|
+
layout: 'side',
|
|
42
|
+
schedule: props?.schedule,
|
|
43
|
+
addToConversation: props?.addToConversation,
|
|
44
|
+
sidebarPanels: props?.sidebarPanels,
|
|
45
|
+
outlineSources: props?.outlineSources,
|
|
46
|
+
fileMenuItems: props?.fileMenuItems,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 右键菜单 viewport 定位纯函数。
|
|
3
|
+
* 将目标行锚点与视口边界计算从 React 渲染层拆出,便于复用和单测。
|
|
4
|
+
* 作者 ddj 2026-08-28
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface MenuPosition {
|
|
8
|
+
left: number
|
|
9
|
+
top: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MenuAnchorRect {
|
|
13
|
+
top: number
|
|
14
|
+
right: number
|
|
15
|
+
width: number
|
|
16
|
+
height: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 将菜单坐标限制在 viewport 内。
|
|
21
|
+
* @author ddj 2026年08月28号
|
|
22
|
+
* @param x 期望的 viewport x 坐标
|
|
23
|
+
* @param y 期望的 viewport y 坐标
|
|
24
|
+
* @param viewportWidth viewport 宽度
|
|
25
|
+
* @param viewportHeight viewport 高度
|
|
26
|
+
* @param menuWidth 菜单估算宽度
|
|
27
|
+
* @param menuHeight 菜单估算高度
|
|
28
|
+
* @returns 限制后的菜单位置
|
|
29
|
+
*/
|
|
30
|
+
export function clampMenuPosition(
|
|
31
|
+
x: number,
|
|
32
|
+
y: number,
|
|
33
|
+
viewportWidth: number,
|
|
34
|
+
viewportHeight: number,
|
|
35
|
+
menuWidth: number,
|
|
36
|
+
menuHeight: number,
|
|
37
|
+
): MenuPosition {
|
|
38
|
+
const maxLeft = Math.max(4, viewportWidth - menuWidth)
|
|
39
|
+
const maxTop = Math.max(4, viewportHeight - menuHeight)
|
|
40
|
+
return {
|
|
41
|
+
left: Math.max(4, Math.min(x, maxLeft)),
|
|
42
|
+
top: Math.max(4, Math.min(y, maxTop)),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 将文件树行右键菜单锚定到目标行右侧;无法测量时回退到鼠标坐标。
|
|
48
|
+
* @author ddj 2026年08月28号
|
|
49
|
+
* @param rect 目标行的布局矩形
|
|
50
|
+
* @param fallbackX 鼠标 viewport x 坐标
|
|
51
|
+
* @param fallbackY 鼠标 viewport y 坐标
|
|
52
|
+
* @returns 菜单初始位置
|
|
53
|
+
*/
|
|
54
|
+
export function rowMenuPosition(
|
|
55
|
+
rect: MenuAnchorRect | null | undefined,
|
|
56
|
+
fallbackX: number,
|
|
57
|
+
fallbackY: number,
|
|
58
|
+
): MenuPosition {
|
|
59
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
|
60
|
+
return { left: fallbackX, top: fallbackY }
|
|
61
|
+
}
|
|
62
|
+
return { left: rect.right + 4, top: rect.top }
|
|
63
|
+
}
|
package/src/rpc.ts
CHANGED
|
@@ -34,6 +34,8 @@ import { revealInExplorer } from './reveal.js'
|
|
|
34
34
|
|
|
35
35
|
/** cwd → Promise 链:串行化 debug 日志追加(fs read+write 非原子,避免并发丢行)。 */
|
|
36
36
|
const debugWriteQueues = new Map<string, Promise<void>>()
|
|
37
|
+
/** debug 日志单文件上限:超限截断保留尾部,防文件无限增长拖慢每次追加。 */
|
|
38
|
+
const DEBUG_LOG_CAP = 512 * 1024
|
|
37
39
|
|
|
38
40
|
/** 记录 → 客户端视图(不含 before 全文,仅长度)。 */
|
|
39
41
|
function recView(record: DiffRecord): RecordView {
|
|
@@ -300,7 +302,11 @@ export function buildHandlers(ctx: Ctx, registry: Registry, searcher = newSearch
|
|
|
300
302
|
const target = await fs.resolve('.dsh-edit-review-debug.log', { cwd: sc.cwd })
|
|
301
303
|
const old = await fs.readText(target).catch(() => '')
|
|
302
304
|
const line = new Date().toISOString() + ' ' + text + '\n'
|
|
303
|
-
|
|
305
|
+
// 超上限时截断保留尾部:单文件体积有界,每次追加的读改写成本不随会话时长增长
|
|
306
|
+
const next = old.length + line.length > DEBUG_LOG_CAP
|
|
307
|
+
? old.slice(-Math.floor(DEBUG_LOG_CAP / 2)) + line
|
|
308
|
+
: old + line
|
|
309
|
+
await fs.writeText(target, next, void 0, void 0, policyOf(ctx, sc.session))
|
|
304
310
|
} catch (e) { /* 写日志失败忽略 */ }
|
|
305
311
|
})
|
|
306
312
|
debugWriteQueues.set(sc.cwd, task)
|