dsh-vscode-mode 0.11.0 → 0.13.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 +158 -111
- package/assets/shell/dsh-open.cs +65 -149
- package/assets/shell/dsh-open.mjs +241 -0
- package/assets/shell/dsh-open.ps1 +28 -47
- package/assets/shell/dsh-open.sh +25 -66
- package/lib/client.js +13362 -11806
- package/lib/client.js.map +1 -1
- package/lib/index.js +4110 -2507
- package/lib/index.js.map +1 -1
- package/package.json +9 -8
- package/skills/dsh-vscodemode-mcp/SKILL.md +94 -132
- package/src/childEnv.ts +19 -0
- package/src/client/activePathStore.ts +28 -0
- package/src/client/addToConversation.ts +4 -2
- package/src/client/ai/inlineProvider.ts +1 -8
- package/src/client/commandBridge.ts +14 -42
- package/src/client/externalOpen.ts +40 -19
- package/src/client/index.ts +101 -18
- package/src/client/keybindings.ts +36 -73
- package/src/client/monaco/loader.ts +3 -1
- package/src/client/openFlow.ts +36 -28
- package/src/client/openReceipt.ts +64 -0
- package/src/client/panelCommands.ts +101 -0
- package/src/client/pdf/pdfLoader.ts +4 -2
- package/src/client/receiptState.ts +120 -0
- package/src/client/rpc.ts +30 -4
- package/src/client/shortcutRecorder.ts +161 -0
- package/src/client/shortcutsOfficial.ts +460 -0
- package/src/client/sidebar/panels/RulesPanel.ts +15 -11
- package/src/client/sidebarBridge.ts +7 -0
- package/src/client/ui/DiffBadge.ts +2 -1
- package/src/client/ui/EditorView.ts +246 -137
- package/src/client/ui/McpSettings.ts +10 -8
- package/src/client/ui/OfficialSideTab.ts +5 -2
- package/src/client/ui/QuickOpen.ts +16 -16
- package/src/client/ui/ShortcutSettings.ts +274 -0
- package/src/client/ui/commandCatalog.ts +39 -43
- package/src/client/useOpenReceipt.ts +128 -0
- package/src/compat.ts +5 -3
- package/src/dap/manager.ts +46 -13
- package/src/devForm.ts +252 -92
- package/src/externalHandoff.ts +93 -52
- package/src/fileOpenSettings.ts +4 -24
- package/src/hostImport.ts +19 -0
- package/src/httpGuard.ts +101 -0
- package/src/index.ts +106 -97
- package/src/integrate.ts +56 -50
- package/src/lsp/transport.ts +84 -127
- package/src/mcp.ts +58 -9
- package/src/mcpIsolation.ts +211 -102
- package/src/mcpProject.ts +220 -133
- package/src/mcpRuntime.ts +343 -0
- package/src/openBridge.ts +69 -0
- package/src/openInbox.ts +207 -0
- package/src/paths.ts +8 -5
- package/src/processTree.ts +63 -0
- package/src/reveal.ts +5 -3
- package/src/routes.ts +156 -154
- package/src/rpc.ts +14 -13
- package/src/rules.ts +100 -44
- package/src/shared/appUrl.ts +19 -0
- package/src/shared/externalOpen.ts +71 -0
- package/src/shared/integration.ts +2 -0
- package/src/shared/keybindings.ts +256 -0
- package/src/shared/mcp.ts +4 -1
- package/src/shared/rpc.ts +7 -4
- package/src/shared/rules.ts +9 -1
- package/src/shared/svn.ts +6 -8
- package/src/svnPlanTool.ts +51 -0
- package/src/unityBridge.ts +26 -3
- package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +99 -203
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +166 -0
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs.meta +2 -0
- package/unity/com.dsh.editor/README.md +16 -25
- package/unity/com.dsh.editor/package.json +2 -2
- package/src/client/ui/KeybindingsPanel.ts +0 -190
|
@@ -8,11 +8,15 @@
|
|
|
8
8
|
* 作者 ddj 2026-08-20
|
|
9
9
|
*/
|
|
10
10
|
import React from 'react'
|
|
11
|
+
import { useOpenReceipt } from '../useOpenReceipt.js'
|
|
12
|
+
import { acceptsLoad, receiptErrorFor, receiptPath, receiptReadyFor, receiptTabPath } from '../receiptState.js'
|
|
13
|
+
import { bindPanelCommand } from '../panelCommands.js'
|
|
11
14
|
import { dbg, rpc } from '../rpc.js'
|
|
12
15
|
import { emitFileChanged, emitRefresh } from '../events.js'
|
|
13
16
|
import { langOf, loadMonaco, snippetLanguageOf } from '../monaco/loader.js'
|
|
14
17
|
import { clampZoom, clearZoomMem, dataUrlOf, isImagePath, isSvgPath, recallZoom, rememberZoom, zoomKeyOf, zoomStepOf } from '../imagePreview.js'
|
|
15
18
|
import { isMarkdownPath } from '../markdownPreview.js'
|
|
19
|
+
import { setActiveEditorPath } from '../activePathStore.js'
|
|
16
20
|
import { MarkdownPanel } from '../md/mdPanel.js'
|
|
17
21
|
import { base64ToBytes, bytesToBase64, isPdfPath } from '../pdfPreview.js'
|
|
18
22
|
import { inNativePath, tryNativeOpen } from '../nativeOpenStore.js'
|
|
@@ -49,7 +53,7 @@ import { upsertViewState, viewStatesLoad, viewStatesSave } from '../state/viewSt
|
|
|
49
53
|
import { migrateScopedKeys, workspaceScopeOf } from '../state/scopeStore.js'
|
|
50
54
|
import { modelsForScope, rememberModel } from '../state/modelCache.js'
|
|
51
55
|
import { navFlashRangeOf } from '../navHighlight.js'
|
|
52
|
-
import {
|
|
56
|
+
import { chordOf, useKeybindingsVersion } from '../keybindings.js'
|
|
53
57
|
import { getSidebarMinWidth } from '../sidebarMin.js'
|
|
54
58
|
import { navHistoryFor } from '../navHistory.js'
|
|
55
59
|
import { statusOfAdd } from '../addToConversation.js'
|
|
@@ -234,6 +238,7 @@ export function EditorView(props) {
|
|
|
234
238
|
const [dirtyMap, setDirtyMap] = React.useState({})
|
|
235
239
|
const [content, setContent] = React.useState(null)
|
|
236
240
|
const [contentPath, setContentPath] = React.useState(null)
|
|
241
|
+
const [receiptLoad, setReceiptLoad] = React.useState(null) // { sessionId, path, sequence, status, error }
|
|
237
242
|
const [imageSrc, setImageSrc] = React.useState(null) // 图片预览 data URL(图片 tab 专用,文本 tab 恒为 null)
|
|
238
243
|
const [imgSize, setImgSize] = React.useState(null) // 图片自然尺寸 { w, h }(路径栏 meta)
|
|
239
244
|
const [imgBroken, setImgBroken] = React.useState(false) // 图片解码失败(onError),显示占位与重试
|
|
@@ -335,6 +340,9 @@ export function EditorView(props) {
|
|
|
335
340
|
const rowNavMoveRef = React.useRef(false) // 本次光标变化是否由整行移动触发(否则清空期望列)
|
|
336
341
|
const activeRef = React.useRef(null) // 当前活动文件的最新值(空依赖闭包/指令回调读取)
|
|
337
342
|
activeRef.current = active
|
|
343
|
+
// 活动文件路径镜像到模块级 store:命令目录可用性判定读取(如 Markdown 预览切换仅 .md 吞键)
|
|
344
|
+
setActiveEditorPath(active)
|
|
345
|
+
React.useEffect(() => () => setActiveEditorPath(null), [])
|
|
338
346
|
const tabsRef = React.useRef([]) // 当前页签表的最新值(菜单动作按 id 分派时读,防陈旧闭包)
|
|
339
347
|
tabsRef.current = tabs
|
|
340
348
|
const tabPathsRef = React.useRef([]) // 页签路径镜像(外部改动轮询读取)
|
|
@@ -800,6 +808,87 @@ export function EditorView(props) {
|
|
|
800
808
|
}).catch((e) => setError('list异常:' + String(e)))
|
|
801
809
|
}
|
|
802
810
|
|
|
811
|
+
/**
|
|
812
|
+
* Reject callbacks for a replaced file or loader generation.
|
|
813
|
+
* @private @author ddj 2026年09月28号
|
|
814
|
+
* @param path Normalized tab path.
|
|
815
|
+
* @param sequence Loader generation.
|
|
816
|
+
* @returns Whether this load still owns the active view.
|
|
817
|
+
*/
|
|
818
|
+
function loadCurrent(path, sequence) {
|
|
819
|
+
return sequence === loadSeqRef.current && path === activeRef.current
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Commit receipt metadata in the same React batch as loaded content.
|
|
824
|
+
* @private @author ddj 2026年09月28号
|
|
825
|
+
* @param path Loaded tab path.
|
|
826
|
+
* @param sid Session owning the operation.
|
|
827
|
+
* @param sequence Loader generation.
|
|
828
|
+
* @param failure Optional target-specific read/render error.
|
|
829
|
+
*/
|
|
830
|
+
function stampLoad(path, sid, sequence, failure) {
|
|
831
|
+
if (!loadCurrent(path, sequence)) return
|
|
832
|
+
setReceiptLoad({ path, cwd, sessionId: sid, sequence, status: failure ? 'error' : 'ready', error: failure })
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Report a read failure without attributing another operation's error to the file.
|
|
837
|
+
* @private @author ddj 2026年09月28号
|
|
838
|
+
* @param path Failed tab path.
|
|
839
|
+
* @param sid Session owning the load.
|
|
840
|
+
* @param sequence Loader generation.
|
|
841
|
+
* @param message Read failure description.
|
|
842
|
+
*/
|
|
843
|
+
function failLoad(path, sid, sequence, message) {
|
|
844
|
+
if (!loadCurrent(path, sequence)) return
|
|
845
|
+
stampLoad(path, sid, sequence, message)
|
|
846
|
+
setLoadError(message)
|
|
847
|
+
setError(message)
|
|
848
|
+
setStatus('读取失败')
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Attribute a preview decoder failure only to its still-current byte generation.
|
|
853
|
+
* @private @author ddj 2026年09月28号
|
|
854
|
+
* @param path Rendered file path.
|
|
855
|
+
* @param sid Rendering session.
|
|
856
|
+
* @param sequence Byte-load generation captured by the renderer.
|
|
857
|
+
* @param message Decoder failure.
|
|
858
|
+
*/
|
|
859
|
+
function renderLoadFail(path, sid, sequence, message) {
|
|
860
|
+
if (!loadCurrent(path, sequence)) return
|
|
861
|
+
/** @author ddj 2026年09月28号 @param current Committed load state. @returns Correlated failure or unchanged state. */
|
|
862
|
+
const reject = (current) => acceptsLoad(current, sid, path, sequence, loadSeqRef.current)
|
|
863
|
+
? { ...current, status: 'error', error: String(message) } : current
|
|
864
|
+
setReceiptLoad(reject)
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Revalidate clean cached text without replacing edits made during the request.
|
|
869
|
+
* @private @author ddj 2026年09月28号
|
|
870
|
+
* @param model Cached text model.
|
|
871
|
+
* @param path Normalized target path.
|
|
872
|
+
* @param sid Session owning the read.
|
|
873
|
+
* @param sequence Loader generation.
|
|
874
|
+
* @param mark Existing disk-version bookkeeping callback.
|
|
875
|
+
*/
|
|
876
|
+
function checkCached(model, path, sid, sequence, mark) {
|
|
877
|
+
if (dirtyRef.current[path]) return
|
|
878
|
+
const before = model.getValue()
|
|
879
|
+
/** @author ddj 2026年09月28号 @param res Disk content for a still-clean cached model. */
|
|
880
|
+
const accept = (res) => {
|
|
881
|
+
if (!loadCurrent(path, sequence) || dirtyRef.current[path] || model.getValue() !== before) return
|
|
882
|
+
if (!res?.ok) return
|
|
883
|
+
mark(res.version)
|
|
884
|
+
if (res.content === before) return
|
|
885
|
+
saveViewState(path)
|
|
886
|
+
setContent(res.content)
|
|
887
|
+
setStatus('已加载')
|
|
888
|
+
}
|
|
889
|
+
rpc('edrv.read', { sessionId: sid, path }).then(accept).catch(() => {})
|
|
890
|
+
}
|
|
891
|
+
|
|
803
892
|
/**
|
|
804
893
|
* 加载文件内容(对齐 VSCode model 复用:会话内已打开的 model 直接秒显,
|
|
805
894
|
* 后台静默 RPC 校验防陈旧;首次打开走原读取流程)。
|
|
@@ -813,6 +902,8 @@ export function EditorView(props) {
|
|
|
813
902
|
*/
|
|
814
903
|
const loadContent = (path, sid, force, version) => {
|
|
815
904
|
const seq = ++loadSeqRef.current
|
|
905
|
+
setReceiptLoad({ path, cwd, sessionId: sid, sequence: seq, status: 'loading' })
|
|
906
|
+
setLoadError(null)
|
|
816
907
|
/**
|
|
817
908
|
* 记下磁盘版本基线;markRead=true 表示本次真的从磁盘读了内容
|
|
818
909
|
* (已读版本台账随之失效,下一轮轮询按新版本重新比对)。
|
|
@@ -841,25 +932,18 @@ export function EditorView(props) {
|
|
|
841
932
|
setContentPath(path)
|
|
842
933
|
setLoadStage((prev) => ({ progress: Math.max(84, prev.progress), message: '文件已读取,准备创建编辑器…' }))
|
|
843
934
|
setStatus('已加载')
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
if (res && res.ok) mark(res.version)
|
|
847
|
-
if (res && res.ok && res.content !== cachedModel.getValue()) {
|
|
848
|
-
saveViewState(path) // 内容更新前保留当前视图位置
|
|
849
|
-
setContent(res.content)
|
|
850
|
-
setStatus('已加载')
|
|
851
|
-
}
|
|
852
|
-
}).catch(() => { /* 校验失败保留缓存内容 */ })
|
|
935
|
+
stampLoad(path, sid, seq)
|
|
936
|
+
checkCached(cachedModel, path, sid, seq, mark)
|
|
853
937
|
return
|
|
854
938
|
}
|
|
855
|
-
setLoadError(null)
|
|
856
939
|
setLoadStage({ progress: monaco ? 72 : 12, message: '读取文件内容…' })
|
|
857
940
|
rpc('edrv.read', { sessionId: sid, path }).then((res) => {
|
|
858
|
-
if (
|
|
941
|
+
if (!loadCurrent(path, seq)) return
|
|
859
942
|
if (res && res.ok) {
|
|
860
943
|
mark(res.version, true)
|
|
861
944
|
setContent(res.content)
|
|
862
945
|
setContentPath(path)
|
|
946
|
+
stampLoad(path, sid, seq)
|
|
863
947
|
setLoadStage((prev) => ({ progress: Math.max(84, prev.progress), message: '文件已读取,准备创建编辑器…' }))
|
|
864
948
|
setStatus('已加载')
|
|
865
949
|
} else {
|
|
@@ -867,16 +951,12 @@ export function EditorView(props) {
|
|
|
867
951
|
dapTrace('read-failed', { path, error: base, resolvedPath: res?.resolvedPath, cwd })
|
|
868
952
|
// 带出 host 解析后的真实路径:跳转失败时一眼看出是路径解析错还是目标不存在
|
|
869
953
|
const message = res?.resolvedPath ? base + ':' + relativeOf(String(res.resolvedPath), cwd) : base
|
|
870
|
-
|
|
871
|
-
setError(message)
|
|
872
|
-
setStatus('读取失败')
|
|
954
|
+
failLoad(path, sid, seq, message)
|
|
873
955
|
}
|
|
874
956
|
}).catch((e) => {
|
|
875
|
-
if (seq
|
|
957
|
+
if (!loadCurrent(path, seq)) return
|
|
876
958
|
const message = 'read异常:' + String(e)
|
|
877
|
-
|
|
878
|
-
setError(message)
|
|
879
|
-
setStatus('读取失败')
|
|
959
|
+
failLoad(path, sid, seq, message)
|
|
880
960
|
})
|
|
881
961
|
}
|
|
882
962
|
|
|
@@ -894,12 +974,13 @@ export function EditorView(props) {
|
|
|
894
974
|
setImgBroken(false)
|
|
895
975
|
setLoadStage({ progress: monaco ? 72 : 12, message: '读取图片…' })
|
|
896
976
|
readBinaryPreview(rpc, { sessionId: sid, path }).then((out) => {
|
|
897
|
-
if (
|
|
977
|
+
if (!loadCurrent(path, seq)) return
|
|
898
978
|
if (out.ok && out.via === 'binary') {
|
|
899
979
|
// 新通道:原始字节本地组 data URL(线上省 base64,本地编码为 img src 所需不可省)
|
|
900
980
|
recordBaseline(scope, path, out.version || version)
|
|
901
981
|
clearReadVersion(scope, path) // 刚读过内容:已读版本台账失效,下一轮按新版本比对
|
|
902
982
|
setImageSrc(dataUrlOf(bytesToBase64(out.bytes), out.mime))
|
|
983
|
+
stampLoad(path, sid, seq)
|
|
903
984
|
setLoadStage({ progress: 100, message: '图片已就绪' })
|
|
904
985
|
setStatus('已加载')
|
|
905
986
|
return
|
|
@@ -907,15 +988,14 @@ export function EditorView(props) {
|
|
|
907
988
|
noteBinFallback(sid, out)
|
|
908
989
|
if (out.ok && out.via === 'base64') {
|
|
909
990
|
if (!out.mime) {
|
|
910
|
-
|
|
911
|
-
setError('host 版本过旧:图片响应缺少 mime')
|
|
912
|
-
setStatus('读取失败')
|
|
991
|
+
failLoad(path, sid, seq, 'host 版本过旧:图片响应缺少 mime')
|
|
913
992
|
return
|
|
914
993
|
}
|
|
915
994
|
const imgVersion = out.version ?? version
|
|
916
995
|
recordBaseline(scope, path, imgVersion)
|
|
917
996
|
clearReadVersion(scope, path) // 刚读过内容:已读版本台账失效,下一轮按新版本比对
|
|
918
997
|
setImageSrc(dataUrlOf(out.content, out.mime))
|
|
998
|
+
stampLoad(path, sid, seq)
|
|
919
999
|
setLoadStage({ progress: 100, message: '图片已就绪' })
|
|
920
1000
|
setStatus('已加载')
|
|
921
1001
|
return
|
|
@@ -923,15 +1003,11 @@ export function EditorView(props) {
|
|
|
923
1003
|
const base = out.error ? String(out.error) : '读取失败'
|
|
924
1004
|
// 带出 host 解析后的真实路径:跳转失败时一眼看出是路径解析错还是目标不存在
|
|
925
1005
|
const message = out.resolvedPath ? base + ':' + String(out.resolvedPath) : base
|
|
926
|
-
|
|
927
|
-
setError(message)
|
|
928
|
-
setStatus('读取失败')
|
|
1006
|
+
failLoad(path, sid, seq, message)
|
|
929
1007
|
}).catch((e) => {
|
|
930
|
-
if (seq
|
|
1008
|
+
if (!loadCurrent(path, seq)) return
|
|
931
1009
|
const message = 'read异常:' + String(e)
|
|
932
|
-
|
|
933
|
-
setError(message)
|
|
934
|
-
setStatus('读取失败')
|
|
1010
|
+
failLoad(path, sid, seq, message)
|
|
935
1011
|
})
|
|
936
1012
|
}
|
|
937
1013
|
|
|
@@ -955,12 +1031,13 @@ export function EditorView(props) {
|
|
|
955
1031
|
cache.delete(path)
|
|
956
1032
|
cache.set(path, hit)
|
|
957
1033
|
setPdfBytes(base64ToBytes(hit))
|
|
1034
|
+
stampLoad(path, sid, seq)
|
|
958
1035
|
setLoadStage({ progress: 100, message: 'PDF 已就绪' })
|
|
959
1036
|
setStatus('已加载')
|
|
960
1037
|
return
|
|
961
1038
|
}
|
|
962
1039
|
readBinaryPreview(rpc, { sessionId: sid, path }).then((out) => {
|
|
963
|
-
if (
|
|
1040
|
+
if (!loadCurrent(path, seq)) return
|
|
964
1041
|
if (out.ok && out.via === 'binary') {
|
|
965
1042
|
const pdfVersion = out.version || version
|
|
966
1043
|
recordBaseline(scope, path, pdfVersion)
|
|
@@ -969,6 +1046,7 @@ export function EditorView(props) {
|
|
|
969
1046
|
cache.set(path, bytesToBase64(out.bytes))
|
|
970
1047
|
while (cache.size > 6) cache.delete(cache.keys().next().value)
|
|
971
1048
|
setPdfBytes(out.bytes)
|
|
1049
|
+
stampLoad(path, sid, seq)
|
|
972
1050
|
setLoadStage({ progress: 100, message: 'PDF 已就绪' })
|
|
973
1051
|
setStatus('已加载')
|
|
974
1052
|
return
|
|
@@ -981,21 +1059,18 @@ export function EditorView(props) {
|
|
|
981
1059
|
cache.set(path, out.content)
|
|
982
1060
|
while (cache.size > 6) cache.delete(cache.keys().next().value)
|
|
983
1061
|
setPdfBytes(base64ToBytes(out.content))
|
|
1062
|
+
stampLoad(path, sid, seq)
|
|
984
1063
|
setLoadStage({ progress: 100, message: 'PDF 已就绪' })
|
|
985
1064
|
setStatus('已加载')
|
|
986
1065
|
return
|
|
987
1066
|
}
|
|
988
1067
|
const base = out.error ? String(out.error) : '读取失败'
|
|
989
1068
|
const message = out.resolvedPath ? base + ':' + String(out.resolvedPath) : base
|
|
990
|
-
|
|
991
|
-
setError(message)
|
|
992
|
-
setStatus('读取失败')
|
|
1069
|
+
failLoad(path, sid, seq, message)
|
|
993
1070
|
}).catch((e) => {
|
|
994
|
-
if (seq
|
|
1071
|
+
if (!loadCurrent(path, seq)) return
|
|
995
1072
|
const message = 'read异常:' + String(e)
|
|
996
|
-
|
|
997
|
-
setError(message)
|
|
998
|
-
setStatus('读取失败')
|
|
1073
|
+
failLoad(path, sid, seq, message)
|
|
999
1074
|
})
|
|
1000
1075
|
}
|
|
1001
1076
|
|
|
@@ -1340,29 +1415,10 @@ export function EditorView(props) {
|
|
|
1340
1415
|
try { localStorage.setItem(sidebarKey, JSON.stringify({ on: sidebarOn, width: sidebarW, panel: activePanel })) }
|
|
1341
1416
|
catch (e) { /* 忽略 */ }
|
|
1342
1417
|
}, [sidebarOn, sidebarW, activePanel, sessionId, sidebarKey])
|
|
1343
|
-
//
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
e.preventDefault(); e.stopPropagation()
|
|
1348
|
-
setSidebarOn((v) => !v)
|
|
1349
|
-
}
|
|
1350
|
-
window.addEventListener('keydown', onKey, true)
|
|
1351
|
-
return () => window.removeEventListener('keydown', onKey, true)
|
|
1352
|
-
}, [])
|
|
1353
|
-
|
|
1354
|
-
// 保存文件(窗口级):编辑器有活动模型才拦截执行,无文件时放行(浏览器默认行为)
|
|
1355
|
-
React.useEffect(() => {
|
|
1356
|
-
const onKey = (e) => {
|
|
1357
|
-
if (!matchEvent(e, bindingsOf('edrv.save'))) return
|
|
1358
|
-
if (!editorRef.current?.getModel?.()) return
|
|
1359
|
-
e.preventDefault(); e.stopPropagation()
|
|
1360
|
-
flushSave()
|
|
1361
|
-
doSaveRef.current(false)
|
|
1362
|
-
}
|
|
1363
|
-
window.addEventListener('keydown', onKey, true)
|
|
1364
|
-
return () => window.removeEventListener('keydown', onKey, true)
|
|
1365
|
-
}, [])
|
|
1418
|
+
// 编辑器键位(侧栏/保存/搜索/Markdown 预览/导航/页签循环)已归官方 shortcuts 机制派发:
|
|
1419
|
+
// 官方 resolve → 指令注册表 run → `edrv.command.*` 事件 → 下方「指令系统接线」表执行。
|
|
1420
|
+
// 历史 capture 监听移除(与官方派发并存会双执行);Markdown 预览的「非 .md 不吞键」
|
|
1421
|
+
// 语义由命令 available()(activePathStore + isMarkdownPath)承载,可用性为假时官方派发放行。
|
|
1366
1422
|
|
|
1367
1423
|
/**
|
|
1368
1424
|
* 打开搜索面板(Ctrl+Shift+F 与命令栏「在工作区中搜索」的唯一动作)。
|
|
@@ -1388,65 +1444,9 @@ export function EditorView(props) {
|
|
|
1388
1444
|
setTimeout(() => window.dispatchEvent(new CustomEvent('edrv:search-focus')), 0)
|
|
1389
1445
|
}
|
|
1390
1446
|
|
|
1391
|
-
|
|
1392
|
-
React.useEffect(() => {
|
|
1393
|
-
const onKey = (e) => {
|
|
1394
|
-
if (!matchEvent(e, bindingsOf('edrv.searchInFiles'))) return
|
|
1395
|
-
e.preventDefault(); e.stopPropagation()
|
|
1396
|
-
openSearchPanel()
|
|
1397
|
-
}
|
|
1398
|
-
window.addEventListener('keydown', onKey, true)
|
|
1399
|
-
return () => window.removeEventListener('keydown', onKey, true)
|
|
1400
|
-
}, [])
|
|
1401
|
-
|
|
1402
|
-
// Ctrl+Shift+V 切换 Markdown 预览:仅活动文件是 Markdown 时吞键;
|
|
1403
|
-
// 其余情况直接放行(不 preventDefault),保留浏览器/输入框原生的「无格式粘贴」语义
|
|
1404
|
-
// ——与 closeTab / addSelectionRef「不可用时放行按键」同款约定。
|
|
1405
|
-
React.useEffect(() => {
|
|
1406
|
-
const onKey = (e) => {
|
|
1407
|
-
if (!matchEvent(e, bindingsOf('edrv.toggleMarkdownPreview'))) return
|
|
1408
|
-
const path = activeRef.current
|
|
1409
|
-
if (!isMarkdownPath(path)) return
|
|
1410
|
-
e.preventDefault(); e.stopPropagation()
|
|
1411
|
-
toggleMdPreviewRef.current?.(path)
|
|
1412
|
-
}
|
|
1413
|
-
window.addEventListener('keydown', onKey, true)
|
|
1414
|
-
return () => window.removeEventListener('keydown', onKey, true)
|
|
1415
|
-
}, [])
|
|
1416
|
-
|
|
1417
|
-
// 后退/前进:导航历史(跨文件焦点位置;经 ref 读最新闭包,避免空依赖过期)
|
|
1418
|
-
React.useEffect(() => {
|
|
1419
|
-
const onKey = (e) => {
|
|
1420
|
-
if (!matchEvent(e, bindingsOf('edrv.navigateBack'))) return
|
|
1421
|
-
e.preventDefault(); e.stopPropagation()
|
|
1422
|
-
if (navBackRef.current) navBackRef.current()
|
|
1423
|
-
}
|
|
1424
|
-
window.addEventListener('keydown', onKey, true)
|
|
1425
|
-
return () => window.removeEventListener('keydown', onKey, true)
|
|
1426
|
-
}, [])
|
|
1427
|
-
React.useEffect(() => {
|
|
1428
|
-
const onKey = (e) => {
|
|
1429
|
-
if (!matchEvent(e, bindingsOf('edrv.navigateForward'))) return
|
|
1430
|
-
e.preventDefault(); e.stopPropagation()
|
|
1431
|
-
if (navForwardRef.current) navForwardRef.current()
|
|
1432
|
-
}
|
|
1433
|
-
window.addEventListener('keydown', onKey, true)
|
|
1434
|
-
return () => window.removeEventListener('keydown', onKey, true)
|
|
1435
|
-
}, [])
|
|
1447
|
+
React.useEffect(() => bindPanelCommand('searchInFiles', { sessionId, cwd: cwd ?? undefined }, openSearchPanel), [sessionId, cwd])
|
|
1436
1448
|
|
|
1437
|
-
//
|
|
1438
|
-
React.useEffect(() => {
|
|
1439
|
-
const onKey = (e) => {
|
|
1440
|
-
const step = matchEvent(e, bindingsOf('edrv.nextTab')) ? 1
|
|
1441
|
-
: matchEvent(e, bindingsOf('edrv.prevTab')) ? -1
|
|
1442
|
-
: 0
|
|
1443
|
-
if (step === 0) return
|
|
1444
|
-
e.preventDefault(); e.stopPropagation()
|
|
1445
|
-
if (cycleTabRef.current) cycleTabRef.current(step)
|
|
1446
|
-
}
|
|
1447
|
-
window.addEventListener('keydown', onKey, true)
|
|
1448
|
-
return () => window.removeEventListener('keydown', onKey, true)
|
|
1449
|
-
}, [])
|
|
1449
|
+
// Ctrl+Shift+F / Ctrl+Shift+V / Alt+←→ / Ctrl+Alt+←→ 的 capture 监听已随官方派发改造移除(见上)。
|
|
1450
1450
|
|
|
1451
1451
|
// 整行上下移动的实现见下方「指令系统接线」effect(moveRow 单点定义,命令栏与键位共用)。
|
|
1452
1452
|
|
|
@@ -1530,7 +1530,6 @@ export function EditorView(props) {
|
|
|
1530
1530
|
['edrv.command.save', () => { if (editorRef.current?.getModel?.()) { flushSave(); doSaveRef.current?.(false) } }],
|
|
1531
1531
|
// 快速打开由 QuickOpen 自己接该事件(它持有搜索框 ref),此处不重复实现
|
|
1532
1532
|
['edrv.command.toggleSidebar', () => setSidebarOn((v) => !v)],
|
|
1533
|
-
['edrv.command.searchInFiles', () => openSearchPanel()],
|
|
1534
1533
|
['edrv.command.toggleMarkdownPreview', () => {
|
|
1535
1534
|
const path = activeRef.current
|
|
1536
1535
|
if (!isMarkdownPath(path)) { setStatus('当前文件不是 Markdown'); return }
|
|
@@ -1826,6 +1825,23 @@ export function EditorView(props) {
|
|
|
1826
1825
|
return true
|
|
1827
1826
|
}
|
|
1828
1827
|
|
|
1828
|
+
/**
|
|
1829
|
+
* Apply a save completion only to the unchanged model that supplied its text.
|
|
1830
|
+
* @private @author ddj 2026年09月28号
|
|
1831
|
+
* @param path Saved tab path.
|
|
1832
|
+
* @param text Text actually sent to disk.
|
|
1833
|
+
* @param model Model captured before the save request.
|
|
1834
|
+
*/
|
|
1835
|
+
function acceptSaved(path, text, model) {
|
|
1836
|
+
if (!model || modelsRef.current.get(path) !== model || model.isDisposed?.() || model.getValue() !== text) return
|
|
1837
|
+
setDirtyMap((current) => ({ ...current, [path]: false }))
|
|
1838
|
+
if (activeRef.current !== path) return
|
|
1839
|
+
setContent(text)
|
|
1840
|
+
setContentPath(path)
|
|
1841
|
+
setStatus('已保存 ' + new Date().toTimeString().slice(0, 8))
|
|
1842
|
+
setDiskFlag((prev) => (prev && prev.path === path ? null : prev))
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1829
1845
|
const doSave = (silent) => {
|
|
1830
1846
|
if (!active) return
|
|
1831
1847
|
// 外部改动待处理:自动保存(silent)直接放弃,避免陈旧缓冲覆盖磁盘;
|
|
@@ -1843,6 +1859,7 @@ export function EditorView(props) {
|
|
|
1843
1859
|
if (!ed) return
|
|
1844
1860
|
if (isImageActive) { setStatus('图片只读预览'); return }
|
|
1845
1861
|
const text = ed.getValue()
|
|
1862
|
+
const savedModel = ed.getModel?.()
|
|
1846
1863
|
const path = active
|
|
1847
1864
|
if (!silent) setStatus('保存中…')
|
|
1848
1865
|
// 在途登记:关闭路径的 persistDirty 据此跳过重复提交(内容在同一 tick 内取,必然相同)
|
|
@@ -1855,13 +1872,7 @@ export function EditorView(props) {
|
|
|
1855
1872
|
recordBaseline(scope, path, res.rev)
|
|
1856
1873
|
markReadVersion(scope, path, res.rev, true) // 缓冲即磁盘内容:同版本无需再读盘
|
|
1857
1874
|
clearSync(scope, path)
|
|
1858
|
-
|
|
1859
|
-
setContentPath(path)
|
|
1860
|
-
setDirtyMap((d) => Object.assign({}, d, { [path]: false }))
|
|
1861
|
-
if (path === active) {
|
|
1862
|
-
setStatus('已保存 ' + new Date().toTimeString().slice(0, 8))
|
|
1863
|
-
setDiskFlag((prev) => (prev && prev.path === path ? null : prev))
|
|
1864
|
-
}
|
|
1875
|
+
acceptSaved(path, text, savedModel)
|
|
1865
1876
|
refreshRecords()
|
|
1866
1877
|
emitRefresh()
|
|
1867
1878
|
// 片段配置文件保存后失效补全缓存(下次补全即读到新片段)
|
|
@@ -1952,7 +1963,7 @@ export function EditorView(props) {
|
|
|
1952
1963
|
syncTypeSuggest(ed, monaco)
|
|
1953
1964
|
restoreViewState(active)
|
|
1954
1965
|
setLoadStage((prev) => ({ progress: Math.max(96, prev.progress), message: '创建编辑器视图…' }))
|
|
1955
|
-
}, [monaco, active, content, contentReady, mdPreviewing])
|
|
1966
|
+
}, [monaco, active, content, contentReady, mdPreviewing, svnDiff])
|
|
1956
1967
|
|
|
1957
1968
|
// PDF 面板外壳 ref 回调(div 仅在 PDF 分支渲染,refs 先于 effect 就绪)
|
|
1958
1969
|
const ensurePdfHost = (node) => { pdfHostRef.current = node }
|
|
@@ -1962,21 +1973,32 @@ export function EditorView(props) {
|
|
|
1962
1973
|
const host = pdfHostRef.current
|
|
1963
1974
|
if (!isPdfActive || !pdfBytes || !host) return
|
|
1964
1975
|
const path = active
|
|
1976
|
+
const sequence = receiptLoad?.sequence
|
|
1977
|
+
if (!acceptsLoad(receiptLoad, sessionId, path, sequence, loadSeqRef.current)) return
|
|
1978
|
+
let mounting = true
|
|
1979
|
+
/** @author ddj 2026年09月28号 @param message PDF error; only startup failures affect an open receipt. */
|
|
1980
|
+
const pdfError = (message) => {
|
|
1981
|
+
setError(message)
|
|
1982
|
+
if (mounting && message) renderLoadFail(path, sessionId, sequence, message)
|
|
1983
|
+
}
|
|
1984
|
+
/** @author ddj 2026年09月28号 End startup error attribution before later save operations. */
|
|
1985
|
+
const mounted = () => { mounting = false }
|
|
1965
1986
|
const ctl = createPdfPanel({
|
|
1966
1987
|
sessionId,
|
|
1967
1988
|
path,
|
|
1968
1989
|
setStatus,
|
|
1969
|
-
setError,
|
|
1990
|
+
setError: pdfError,
|
|
1970
1991
|
onDirtyChange: (d) => setDirtyMap((prev) => Object.assign({}, prev, { [path]: d })),
|
|
1971
1992
|
onReload: () => loadContent(path, sessionId, true),
|
|
1972
1993
|
})
|
|
1973
1994
|
pdfCtlRef.current.set(path, ctl)
|
|
1974
|
-
void ctl.mount(host, pdfBytes)
|
|
1995
|
+
void ctl.mount(host, pdfBytes).then(mounted, pdfError)
|
|
1975
1996
|
return () => {
|
|
1997
|
+
mounting = false
|
|
1976
1998
|
ctl.destroy()
|
|
1977
1999
|
if (pdfCtlRef.current.get(path) === ctl) pdfCtlRef.current.delete(path)
|
|
1978
2000
|
}
|
|
1979
|
-
}, [isPdfActive, pdfBytes, active])
|
|
2001
|
+
}, [isPdfActive, pdfBytes, active, receiptLoad?.sequence])
|
|
1980
2002
|
|
|
1981
2003
|
// 行内差异自绘(decorations / view zones / minus overlay)→ diffRenderer
|
|
1982
2004
|
// ⚠️ 依赖含 mdPreviewing:切回源码时编辑器实例被重建(见上方 model 同步 effect 的说明),
|
|
@@ -1985,7 +2007,7 @@ export function EditorView(props) {
|
|
|
1985
2007
|
if (!monaco || !editorRef.current || !active || content === null) return
|
|
1986
2008
|
diffRendererRef.current.render(monaco, editorRef.current, pendingRegions, sessionId)
|
|
1987
2009
|
setLoadStage({ progress: 100, message: '编辑器已就绪' })
|
|
1988
|
-
}, [monaco, active, content, pendingRegions, mdPreviewing])
|
|
2010
|
+
}, [monaco, active, content, pendingRegions, mdPreviewing, svnDiff])
|
|
1989
2011
|
|
|
1990
2012
|
React.useEffect(() => () => {
|
|
1991
2013
|
flushSave()
|
|
@@ -2362,7 +2384,7 @@ export function EditorView(props) {
|
|
|
2362
2384
|
ed.revealLineInCenter(Math.max(1, target.start ?? 1))
|
|
2363
2385
|
ed.setPosition({ lineNumber: Math.max(1, target.start ?? 1), column: 1 })
|
|
2364
2386
|
ed.focus()
|
|
2365
|
-
}, [active, content, contentReady, pendingRegions, focusRequest, monaco, mdPreviewing])
|
|
2387
|
+
}, [active, content, contentReady, pendingRegions, focusRequest, monaco, mdPreviewing, svnDiff])
|
|
2366
2388
|
|
|
2367
2389
|
const jumpTo = (region) => {
|
|
2368
2390
|
if (editorRef.current) {
|
|
@@ -2546,6 +2568,16 @@ export function EditorView(props) {
|
|
|
2546
2568
|
applyImgZoom(zoomStepOf(imgZoom ?? measured, dir))
|
|
2547
2569
|
}
|
|
2548
2570
|
|
|
2571
|
+
/**
|
|
2572
|
+
* Preserve image UI behavior while attributing decoder failure to its rendered load.
|
|
2573
|
+
* @private @author ddj 2026年09月28号
|
|
2574
|
+
*/
|
|
2575
|
+
function imageFailed() {
|
|
2576
|
+
if (!receiptLoad || !loadCurrent(receiptLoad.path, receiptLoad.sequence)) return
|
|
2577
|
+
setImgBroken(true)
|
|
2578
|
+
renderLoadFail(receiptLoad.path, receiptLoad.sessionId, receiptLoad.sequence, '图片解码失败')
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2549
2581
|
/**
|
|
2550
2582
|
* 图片预览面板:工具条(尺寸 meta / 缩放 −+适应宽度 / SVG 文本切换 / 刷新)+ 棋盘底图片。
|
|
2551
2583
|
* 初始适应宽度(等价 PDF page-width,0.1.7 官方统一缩放);按钮样式与 PDF 工具条同款。
|
|
@@ -2583,13 +2615,14 @@ export function EditorView(props) {
|
|
|
2583
2615
|
: React.createElement('img', {
|
|
2584
2616
|
className: 'edrv-imgview-img',
|
|
2585
2617
|
ref: imgElRef,
|
|
2618
|
+
key: active + ':' + (receiptLoad?.sequence ?? 0),
|
|
2586
2619
|
src: imageSrc,
|
|
2587
2620
|
alt: active || '',
|
|
2588
2621
|
style: (imgZoom !== null && imgSize)
|
|
2589
2622
|
? { width: Math.round(imgSize.w * imgZoom) + 'px', maxWidth: 'none', maxHeight: 'none' }
|
|
2590
2623
|
: undefined,
|
|
2591
2624
|
onLoad: (e) => { setImgBroken(false); setImgSize({ w: e.target.naturalWidth, h: e.target.naturalHeight }) },
|
|
2592
|
-
onError:
|
|
2625
|
+
onError: imageFailed,
|
|
2593
2626
|
}))))
|
|
2594
2627
|
|
|
2595
2628
|
/**
|
|
@@ -3191,13 +3224,89 @@ export function EditorView(props) {
|
|
|
3191
3224
|
// G9:同 openFile,先归一再进页签,保证待跳转路径与 active 同形态
|
|
3192
3225
|
const normalized = addTabNorm(path, true)
|
|
3193
3226
|
if (!normalized) return
|
|
3227
|
+
stageFileAt(normalized, line, column, endLine, endColumn)
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
/**
|
|
3231
|
+
* Stage a normalized navigation target using the existing reload-safe handoff.
|
|
3232
|
+
* @private @author ddj 2026年09月28号
|
|
3233
|
+
* @param normalized Exact tab path.
|
|
3234
|
+
* @param line Optional line.
|
|
3235
|
+
* @param column Optional column.
|
|
3236
|
+
* @param endLine Optional range end line.
|
|
3237
|
+
* @param endColumn Optional range end column.
|
|
3238
|
+
*/
|
|
3239
|
+
function stageFileAt(normalized, line, column, endLine, endColumn) {
|
|
3194
3240
|
pendingFocusRef.current = { path: normalized, region: null, line: line ?? null, column: column ?? 1, endLine: endLine ?? null, endColumn: endColumn ?? null }
|
|
3195
|
-
// 第三次事故(2026-09-23):插件 HMR 热重载会重挂载本组件、挂载级 ref 随旧实例销毁 →
|
|
3196
|
-
// 落点意图丢失。同步落 window 槽(跨热重载存活)做交接,落点成功后与 ref 一并清。
|
|
3197
3241
|
putPendingNav({ path: normalized, line: line ?? null, column: column ?? 1, endLine: endLine ?? null, endColumn: endColumn ?? null })
|
|
3198
3242
|
setFocusRequest((value) => value + 1)
|
|
3199
3243
|
}
|
|
3200
3244
|
|
|
3245
|
+
/**
|
|
3246
|
+
* Open a receipt target without losing edits or leaving the requested source hidden.
|
|
3247
|
+
* @private @author ddj 2026年09月28号
|
|
3248
|
+
* @param request Claimed external open request.
|
|
3249
|
+
*/
|
|
3250
|
+
function openReceiptFile(request) {
|
|
3251
|
+
if (!request.path) return
|
|
3252
|
+
let path = receiptTabPath(request.path, cwd)
|
|
3253
|
+
const existing = tabsRef.current.find((tab) => receiptPath(tab.path, cwd) === receiptPath(path, cwd))
|
|
3254
|
+
if (existing) path = existing.path
|
|
3255
|
+
flushSave()
|
|
3256
|
+
saveViewState(active)
|
|
3257
|
+
recordNav()
|
|
3258
|
+
setSvnDiff(null)
|
|
3259
|
+
const positioned = request.line != null || request.column != null
|
|
3260
|
+
if (positioned && mdPreviewRef.current.has(path)) toggleMdPreview(path)
|
|
3261
|
+
setError(null)
|
|
3262
|
+
if (monacoErr) setMonacoErr(null)
|
|
3263
|
+
addTab(path, true)
|
|
3264
|
+
if (positioned) stageFileAt(path, request.line ?? 1, request.column ?? 1)
|
|
3265
|
+
const moved = receiptLoad && receiptPath(receiptLoad.path, receiptLoad.cwd) !== receiptPath(path, cwd)
|
|
3266
|
+
if (path !== active || !(loadError || receiptLoad?.status === 'error' || moved)) return
|
|
3267
|
+
if (pdfCtlRef.current.get(path)?.isDirty?.()) return
|
|
3268
|
+
pdfB64CacheRef.current.delete(path)
|
|
3269
|
+
loadContent(path, sessionId, false)
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
/**
|
|
3273
|
+
* Build facts from the current render; model identity is the exact cached instance.
|
|
3274
|
+
* @private @author ddj 2026年09月28号
|
|
3275
|
+
* @param request Claimed target and optional position.
|
|
3276
|
+
* @returns State consumed by the tested receipt predicates.
|
|
3277
|
+
*/
|
|
3278
|
+
function receiptView(request) {
|
|
3279
|
+
const ed = editorRef.current
|
|
3280
|
+
const model = ed?.getModel?.()
|
|
3281
|
+
const modelMatches = Boolean(model && modelsRef.current.get(active) === model)
|
|
3282
|
+
let positionMatches = false
|
|
3283
|
+
if (modelMatches && (request.line != null || request.column != null)) {
|
|
3284
|
+
const expected = model.validatePosition({ lineNumber: request.line ?? 1, column: request.column ?? 1 })
|
|
3285
|
+
const actual = ed.getPosition()
|
|
3286
|
+
positionMatches = actual?.lineNumber === expected.lineNumber && actual?.column === expected.column
|
|
3287
|
+
}
|
|
3288
|
+
const image = imgElRef.current
|
|
3289
|
+
return {
|
|
3290
|
+
sessionId, cwd, active, sequence: loadSeqRef.current, load: receiptLoad,
|
|
3291
|
+
mounted: Boolean(viewRootRef.current), blocked: Boolean(svnDiff),
|
|
3292
|
+
kind: isImageActive ? 'image' : isPdfActive ? 'pdf' : mdPreviewing ? 'markdown' : 'text',
|
|
3293
|
+
contentPath, contentReady, modelMatches, positionMatches,
|
|
3294
|
+
imageLoaded: Boolean(imageSrc && !imgBroken && image?.getAttribute('src') === imageSrc && image.complete && image.naturalWidth > 0),
|
|
3295
|
+
pdfLoaded: Boolean(pdfBytes && pdfHostRef.current?.querySelector('.page[data-loaded="true"]')),
|
|
3296
|
+
engineError: monacoErr ? String(monacoErr) : null,
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
/** @private @author ddj 2026年09月28号 @param request Claimed target. @returns Correlated actual readiness. */
|
|
3301
|
+
function receiptReady(request) {
|
|
3302
|
+
return receiptReadyFor(request, receiptView(request))
|
|
3303
|
+
}
|
|
3304
|
+
/** @private @author ddj 2026年09月28号 @param request Claimed target. @returns Only its current load/render error. */
|
|
3305
|
+
function receiptError(request) {
|
|
3306
|
+
return receiptErrorFor(request, receiptView(request))
|
|
3307
|
+
}
|
|
3308
|
+
useOpenReceipt({ sessionId, open: openReceiptFile, ready: receiptReady, error: receiptError })
|
|
3309
|
+
|
|
3201
3310
|
// 停帧跳转/调试命令监听(openFileAt 每渲染重建,经 ref 让空依赖监听读最新闭包)
|
|
3202
3311
|
const dapOpenRef = React.useRef(null)
|
|
3203
3312
|
dapOpenRef.current = openFileAt
|
|
@@ -3493,7 +3602,7 @@ export function EditorView(props) {
|
|
|
3493
3602
|
} else if (isImageActive) {
|
|
3494
3603
|
body = loadingBody(loadStage.message || '读取图片…', loadStage.progress)
|
|
3495
3604
|
} else if (isPdfActive && pdfBytes) {
|
|
3496
|
-
body = React.createElement('div', { className: 'edrv-pdf-host', ref: ensurePdfHost, key: active })
|
|
3605
|
+
body = React.createElement('div', { className: 'edrv-pdf-host', ref: ensurePdfHost, key: active + ':' + (receiptLoad?.sequence ?? 0) })
|
|
3497
3606
|
} else if (isPdfActive) {
|
|
3498
3607
|
body = loadingBody(loadStage.message || '读取 PDF…', loadStage.progress)
|
|
3499
3608
|
} else if (mdPreviewing && contentReady) {
|