dsh-vscode-mode 0.15.8 → 0.15.9
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 +4 -3
- package/lib/client.js +312 -69
- package/lib/client.js.map +1 -1
- package/lib/index.js +195 -0
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +4 -0
- package/src/client/state/editorFindState.ts +15 -10
- package/src/client/state/editorViewState.ts +7 -4
- package/src/client/styles/editor.css +3 -0
- package/src/client/ui/EditorPane.ts +23 -3
- package/src/client/ui/EditorView.ts +117 -15
- package/src/client/ui/McpSettings.ts +36 -0
- package/src/client/updateNotice.ts +64 -0
- package/src/rpc.ts +9 -0
- package/src/selfUpdate.ts +188 -0
- package/src/shared/rpc.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.9",
|
|
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
|
@@ -67,6 +67,7 @@ import { disposeAiInline } from './ai/inlineProvider.js'
|
|
|
67
67
|
import { readSessionScope, subscribeScope } from './sessionScope.js'
|
|
68
68
|
import { installPlanBridge } from './planBridge.js'
|
|
69
69
|
import { installPageReload } from './pageReload.js'
|
|
70
|
+
import { installUpdateNotice } from './updateNotice.js'
|
|
70
71
|
import { flushPageState, flushUiState, hydrateUiState, installUiStatePersist, onUiStateReady, reportUiStateEvent, uiStateSettled } from './state/uiStatePersist.js'
|
|
71
72
|
import { workspaceScopeOf } from './state/scopeStore.js'
|
|
72
73
|
import { dapStore } from './dap/store.js'
|
|
@@ -195,6 +196,9 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
195
196
|
}
|
|
196
197
|
ctx.effect(() => installPageReload(flushPageState, notifyReload), 'vscode-mode: guarded page reload')
|
|
197
198
|
|
|
199
|
+
// 可用更新主动提醒:启动延迟 + 抖动后检查 registry,有新版本且未提醒过时 toast(失败静默)
|
|
200
|
+
installUpdateNotice(ctx, schedule)
|
|
201
|
+
|
|
198
202
|
const registry: FileOpenerRegistry = createFileOpenerRegistry()
|
|
199
203
|
const workspaces = ctx.get('workspaces')
|
|
200
204
|
const sessions = ctx.get('sessions')
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-local Ctrl+F state bridge; ddj 2026年10月09号.
|
|
3
|
+
*
|
|
4
|
+
* 快照键语义(ddj 2026年10月16号):键由调用方传入(findKey),不再限定为工作区 scope——
|
|
5
|
+
* 编辑器按分组隔离搜索框时传「scope + 分组 id」组合键;旧调用传 scope 时行为完全不变。
|
|
6
|
+
*/
|
|
2
7
|
export interface FindSnapshot {
|
|
3
8
|
searchString: string
|
|
4
9
|
isRevealed: boolean
|
|
@@ -58,17 +63,17 @@ function applyFind(controller: FindController, saved: FindSnapshot): Promise<unk
|
|
|
58
63
|
/** @public @author ddj 2026年10月09号 @param editor Monaco editor. @returns Model-switch bridge; dispose releases the native listener. */
|
|
59
64
|
export function createFindBridge(editor: FindEditor) {
|
|
60
65
|
const controller = controllerOf(editor)
|
|
61
|
-
let
|
|
66
|
+
let findKey: string | null = null
|
|
62
67
|
let paused = true
|
|
63
68
|
let disposed = false
|
|
64
69
|
|
|
65
|
-
/** @private @author ddj 2026年10月
|
|
70
|
+
/** @private @author ddj 2026年10月16号 Capture only user changes into the current findKey bucket, not model-reset or restoration events. */
|
|
66
71
|
function capture(): void {
|
|
67
|
-
if (disposed || paused || !
|
|
72
|
+
if (disposed || paused || !findKey || !controller) return
|
|
68
73
|
try {
|
|
69
74
|
const saved = snapshotOf(controller.getState())
|
|
70
|
-
snapshots.delete(
|
|
71
|
-
snapshots.set(
|
|
75
|
+
snapshots.delete(findKey)
|
|
76
|
+
snapshots.set(findKey, saved)
|
|
72
77
|
if (snapshots.size > 20) snapshots.delete(snapshots.keys().next().value!)
|
|
73
78
|
} catch { /* A disposed contribution must not interrupt view capture. */ }
|
|
74
79
|
}
|
|
@@ -77,11 +82,11 @@ export function createFindBridge(editor: FindEditor) {
|
|
|
77
82
|
/** @public @author ddj 2026年10月09号 Save before setModel; suppress Monaco's per-model option resets. */
|
|
78
83
|
function pause(): void { capture(); paused = true }
|
|
79
84
|
|
|
80
|
-
/** @public @author ddj 2026年10月
|
|
81
|
-
function resume(
|
|
85
|
+
/** @public @author ddj 2026年10月16号 @param nextKey Bucket owning the bound view(scope 或 scope+分组 id 组合键). Restore with moveCursor=false; no per-file range or match index. */
|
|
86
|
+
function resume(nextKey: string): void {
|
|
82
87
|
if (disposed || !controller) return
|
|
83
|
-
|
|
84
|
-
const saved = snapshots.get(
|
|
88
|
+
findKey = nextKey
|
|
89
|
+
const saved = snapshots.get(findKey) ?? {
|
|
85
90
|
searchString: '', isRevealed: false, matchCase: false, wholeWord: false, isRegex: false,
|
|
86
91
|
}
|
|
87
92
|
try {
|
|
@@ -14,6 +14,8 @@ export interface ViewEditor extends FindEditor {
|
|
|
14
14
|
export function createViewKeeper(editor: ViewEditor) {
|
|
15
15
|
const find = createFindBridge(editor)
|
|
16
16
|
let bound: { scope: string; path: string; model: unknown } | null = null
|
|
17
|
+
// Ctrl+F 快照键跟随最近一次 bind(默认回退 scope);编辑器跨分组复用时按组存取搜索框状态
|
|
18
|
+
let lastFindKey: string | null = null
|
|
17
19
|
let disposed = false
|
|
18
20
|
|
|
19
21
|
/** @public @author ddj 2026年10月09号 @param path Optional expected path; mismatches never write a snapshot under the wrong file. Persist viewport even if cursor never moved. */
|
|
@@ -27,20 +29,21 @@ export function createViewKeeper(editor: ViewEditor) {
|
|
|
27
29
|
} catch { /* A missing/disposed model must not overwrite a valid snapshot. */ }
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
/** @public @author ddj 2026年10月
|
|
31
|
-
function bind(scope: string, path: string, model: unknown): void {
|
|
32
|
+
/** @public @author ddj 2026年10月16号 @param scope Model workspace. @param path Cache path. @param model Ready text model. @param findKey Optional Ctrl+F snapshot bucket(scope+分组 id);缺省沿用 scope. Restore only on identity change, before explicit navigation effects. */
|
|
33
|
+
function bind(scope: string, path: string, model: unknown, findKey?: string): void {
|
|
32
34
|
if (disposed || !path || !model) return
|
|
33
35
|
if (bound?.scope === scope && bound.path === path && bound.model === model
|
|
34
|
-
&& editor.getModel() === model) return
|
|
36
|
+
&& editor.getModel() === model && (findKey === undefined || findKey === lastFindKey)) return
|
|
35
37
|
save()
|
|
36
38
|
find.pause()
|
|
37
39
|
if (editor.getModel() !== model) editor.setModel(model)
|
|
38
40
|
bound = { scope, path, model }
|
|
41
|
+
lastFindKey = findKey ?? scope
|
|
39
42
|
const state = viewStatesLoad(scope)[path]
|
|
40
43
|
if (state) {
|
|
41
44
|
try { editor.restoreViewState(state) } catch { /* Stale fold ranges may be invalid. */ }
|
|
42
45
|
}
|
|
43
|
-
find.resume(
|
|
46
|
+
find.resume(lastFindKey)
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
/** @public @author ddj 2026年10月09号 Save before dispose/ref clearing; repeated cleanup is a no-op. */
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
[data-edrv-view] .edrv-pathbar .edrv-pb-full { flex: 1; min-width: 0; color: var(--dsw-alias-label-tertiary, #9aa5b1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; direction: rtl; text-align: left; }
|
|
24
24
|
[data-edrv-view] .edrv-pathbar .edrv-pb-meta { flex-shrink: 0; font-size: 11px; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-variant-numeric: tabular-nums; }
|
|
25
25
|
[data-edrv-view] .edrv-pathbar .edrv-pb-status { color: var(--dsw-alias-label-secondary, #52606d); }
|
|
26
|
+
/* 非焦点分组的静态路径条(GroupPathbar):可点击聚焦所属组,悬停给反馈 @author ddj 2026-10-16 */
|
|
27
|
+
[data-edrv-view] .edrv-pathbar[data-edrv-pb-static] { cursor: pointer; }
|
|
28
|
+
[data-edrv-view] .edrv-pathbar[data-edrv-pb-static]:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.06)); }
|
|
26
29
|
[data-edrv-view] .edrv-pb-update { display: inline-flex; align-items: center; gap: 4px; height: 20px; flex: none; padding: 0 6px; border: 1px solid var(--dsw-alias-border-l1, #d9e0e4); border-radius: 4px; background: var(--dsw-alias-bg-layer-2, #f1f4f5); color: var(--dsw-alias-label-secondary, #485762); font-size: 11px; cursor: pointer; }
|
|
27
30
|
[data-edrv-view] .edrv-pb-update:hover { background: var(--dsw-alias-interactive-bg-hover, #e7edf0); }
|
|
28
31
|
[data-edrv-view] .edrv-pb-update:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #2869b4); outline-offset: 1px; }
|
|
@@ -61,14 +61,34 @@ export function bindPaneView(editor: ViewEditor, scope: string, path: string, mo
|
|
|
61
61
|
|
|
62
62
|
export interface PassiveProps {
|
|
63
63
|
path: string | null
|
|
64
|
-
|
|
64
|
+
groupId?: string
|
|
65
|
+
mount(host: HTMLDivElement, path: string, groupId?: string): (() => void) | undefined
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* 非焦点分组的静态路径条:让路径条在每个编辑分组常驻(分组高度不随焦点切换变化)。
|
|
70
|
+
* 只读展示文件名 + 完整路径,点击聚焦所属组(与 passive 编辑器 pointerdown 同语义)。
|
|
71
|
+
* @public @author ddj 2026年10月16号
|
|
72
|
+
* @param props path 当前组活动文件(null 显示占位);onFocus 点击回调。
|
|
73
|
+
* @returns 恒定 26px 高的路径条元素。
|
|
74
|
+
*/
|
|
75
|
+
export function GroupPathbar(props: { path: string | null; onFocus?(): void }): React.ReactElement {
|
|
76
|
+
const name = props.path ? props.path.split(/[\\/]/).pop() : '未打开文件'
|
|
77
|
+
return React.createElement('div', {
|
|
78
|
+
className: 'edrv-pathbar', 'data-edrv-pb-static': '1',
|
|
79
|
+
title: props.path || '此分页未打开文件', onClick: props.onFocus,
|
|
80
|
+
},
|
|
81
|
+
React.createElement('span', { className: 'edrv-pb-name' }, name),
|
|
82
|
+
React.createElement('span', { className: 'edrv-pb-full' }, props.path || ''))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @public @author ddj 2026年10月16号 @param props Workbench-owned mounting contract. @returns Independent visible file view without global command listeners. */
|
|
68
86
|
export function PassiveEditor(props: PassiveProps): React.ReactElement {
|
|
69
87
|
const host = React.useRef<HTMLDivElement>(null)
|
|
88
|
+
const groupRef = React.useRef(props.groupId)
|
|
89
|
+
groupRef.current = props.groupId
|
|
70
90
|
React.useLayoutEffect(() => {
|
|
71
|
-
if (host.current && props.path) return props.mount(host.current, props.path)
|
|
91
|
+
if (host.current && props.path) return props.mount(host.current, props.path, groupRef.current)
|
|
72
92
|
}, [props.path, props.mount])
|
|
73
93
|
return React.createElement('div', { ref: host, className: 'edrv-passive-view' })
|
|
74
94
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import React from 'react'
|
|
11
11
|
import { createPortal } from 'react-dom'
|
|
12
|
-
import { EditorPane, EditorSplit, PassiveEditor } from './EditorPane.js'
|
|
12
|
+
import { EditorPane, EditorSplit, PassiveEditor, GroupPathbar } from './EditorPane.js'
|
|
13
13
|
import { FloatingEditor } from './FloatingEditor.js'
|
|
14
14
|
import { FloatingWindow } from './FloatingWindow.js'
|
|
15
15
|
import { createLayout, parseLayout, syncTabs, ownerOf, focusGroup, moveTab, returnGroup, GROUP_LIMIT, MAIN_GROUP } from '../state/editorLayout.js'
|
|
@@ -1898,6 +1898,37 @@ export function EditorView(props) {
|
|
|
1898
1898
|
loadContent(active, sessionId)
|
|
1899
1899
|
}, [active, sessionId])
|
|
1900
1900
|
|
|
1901
|
+
/**
|
|
1902
|
+
* 切换焦点分组时同步重置内容状态(与 setWorkbench 同批渲染)。
|
|
1903
|
+
*
|
|
1904
|
+
* 为什么不能只靠上面的 active effect:它在本轮 commit **之后**才执行——而 mountFull 在 commit 阶段
|
|
1905
|
+
* 就把 fullHost 搬进新焦点组,导致新组先 paint 一帧旧组文件内容(用户所见「内容串组」闪烁)。
|
|
1906
|
+
*
|
|
1907
|
+
* 两个分支(ddj 2026年10月16号):
|
|
1908
|
+
* - 新组文件 model 已缓存 → 直接用缓存内容 seed(同批渲染即最终内容,无 loading 帧 → 切组零空白);
|
|
1909
|
+
* - 未缓存 → setContent(null)(loading 帧),避免旧内容串组。
|
|
1910
|
+
* 之后 active effect 的 loadContent 照常校验/接管(缓存分支 value 相同,React 不会多渲染)。
|
|
1911
|
+
*/
|
|
1912
|
+
const resetContentForSwitch = (nextPath) => {
|
|
1913
|
+
const cached = nextPath ? modelsRef.current.get(nextPath) : null
|
|
1914
|
+
if (cached) {
|
|
1915
|
+
setContent(cached.getValue())
|
|
1916
|
+
setContentPath(nextPath)
|
|
1917
|
+
setLoadStage((prev) => ({ progress: Math.max(prev.progress, 84), message: '文件已读取,准备创建编辑器…' }))
|
|
1918
|
+
setStatus('已加载')
|
|
1919
|
+
return
|
|
1920
|
+
}
|
|
1921
|
+
setContent(null)
|
|
1922
|
+
setContentPath(null)
|
|
1923
|
+
setLoadStage({ progress: 10, message: '准备读取文件…' })
|
|
1924
|
+
setStatus('加载中…')
|
|
1925
|
+
setImageSrc(null)
|
|
1926
|
+
setImgSize(null)
|
|
1927
|
+
setImgBroken(false)
|
|
1928
|
+
setPdfBytes(null)
|
|
1929
|
+
setLoadError(null)
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1901
1932
|
// 页签右键菜单的 Esc 关闭由 ContextMenu 组件自己处理(capture 监听),此处不再重复挂监听
|
|
1902
1933
|
|
|
1903
1934
|
React.useEffect(() => {
|
|
@@ -2191,7 +2222,7 @@ export function EditorView(props) {
|
|
|
2191
2222
|
if (!monaco || !editorRef.current || !active || !contentReady) return
|
|
2192
2223
|
const ed = editorRef.current
|
|
2193
2224
|
const model = getModel(active, content)
|
|
2194
|
-
viewKeeperRef.current?.bind(String(scope), active, model)
|
|
2225
|
+
viewKeeperRef.current?.bind(String(scope), active, model, findKeyOf(scope, focusedGroupOf(active)))
|
|
2195
2226
|
syncTypeSuggest(ed, monaco)
|
|
2196
2227
|
setLoadStage((prev) => ({ progress: Math.max(96, prev.progress), message: '创建编辑器视图…' }))
|
|
2197
2228
|
}, [monaco, scope, active, content, contentReady, mdPreviewing, svnDiff])
|
|
@@ -2291,8 +2322,9 @@ export function EditorView(props) {
|
|
|
2291
2322
|
language: 'plaintext',
|
|
2292
2323
|
theme: themeNameOf(),
|
|
2293
2324
|
fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)',
|
|
2294
|
-
|
|
2295
|
-
|
|
2325
|
+
// 16px/21px:与 md 预览渲染态同字号,切分页/切组时观感不再跳变(ddj 2026-10-16)
|
|
2326
|
+
fontSize: 16,
|
|
2327
|
+
lineHeight: 21,
|
|
2296
2328
|
minimap: { enabled: !side, scale: 1 },
|
|
2297
2329
|
glyphMargin: true,
|
|
2298
2330
|
lineDecorationsWidth: 16,
|
|
@@ -3704,16 +3736,21 @@ export function EditorView(props) {
|
|
|
3704
3736
|
saveViewState()
|
|
3705
3737
|
recordNav()
|
|
3706
3738
|
const next = focusGroup(benchRef.current, group, path)
|
|
3739
|
+
const nextActive = next.groups.find((item) => item.id === group)?.active ?? null
|
|
3740
|
+
// 新焦点组文件与当前 contentPath 不同才重置:同文件聚焦无内容切换;缓存命中同步 seed(零空白),未命中清空防串组
|
|
3741
|
+
if (nextActive && nextActive !== activeRef.current) resetContentForSwitch(nextActive)
|
|
3707
3742
|
setWorkbench(next)
|
|
3708
3743
|
setPlanActive(null)
|
|
3709
|
-
setDiskActive(
|
|
3744
|
+
setDiskActive(nextActive)
|
|
3710
3745
|
}
|
|
3711
3746
|
/** @private @author ddj 2026年10月14号 @param next Atomic layout. Accept ownership without close/save side effects. */
|
|
3712
3747
|
function acceptLayout(next) {
|
|
3713
3748
|
saveViewState()
|
|
3749
|
+
const nextActive = next.groups.find((group) => group.id === next.focused)?.active ?? null
|
|
3750
|
+
if (nextActive && nextActive !== activeRef.current) resetContentForSwitch(nextActive)
|
|
3714
3751
|
setWorkbench(next)
|
|
3715
3752
|
setPlanActive(null)
|
|
3716
|
-
setDiskActive(
|
|
3753
|
+
setDiskActive(nextActive)
|
|
3717
3754
|
}
|
|
3718
3755
|
/** @private @author ddj 2026年10月14号 @param path File. @param edge Split direction. Move the file into a new group. */
|
|
3719
3756
|
function splitTab(path, edge) {
|
|
@@ -3765,9 +3802,62 @@ export function EditorView(props) {
|
|
|
3765
3802
|
}
|
|
3766
3803
|
/** @private @author ddj 2026年10月14号 @param host Group body. Keep the full editor subtree stable while changing group/window ownership. */
|
|
3767
3804
|
function mountFull(host) { if (host && fullHost.parentElement !== host) { host.appendChild(fullHost); editorRef.current?.layout() } }
|
|
3768
|
-
/** @private @author ddj 2026年10月
|
|
3769
|
-
function
|
|
3770
|
-
|
|
3805
|
+
/** @private @author ddj 2026年10月16号 @param editor Passive editor instance. @param host Passive view container. @param path Canonical file of this group. @returns Gutter cleanup(断点装饰订阅 + 右键捕获解除). 断点 glyph 列每分页可用:装饰/左键切换/hover 预览/右键菜单与焦点编辑器同语义。 */
|
|
3806
|
+
function bindBpGutter(editor, host, path) {
|
|
3807
|
+
const m = window.monaco
|
|
3808
|
+
const GUTTER = m.editor?.MouseTargetType?.GUTTER_GLYPH_MARGIN ?? 2
|
|
3809
|
+
const bpListOf = () => (dapStore.bpMapOf(dapScopeRef.current)[path] ?? [])
|
|
3810
|
+
|
|
3811
|
+
const applyBp = () => {
|
|
3812
|
+
const map = dapStore.bpMapOf(dapScopeRef.current)
|
|
3813
|
+
const unverified = bpLinesOf(map, path).filter((line) => dapStore.ackOf(path, line)?.verified === false)
|
|
3814
|
+
applyBpDecorations(editor, bpLinesOf(map, path), bpDisabledLinesOf(map, path), unverified)
|
|
3815
|
+
}
|
|
3816
|
+
applyBp()
|
|
3817
|
+
const unsubscribeBp = dapStore.subscribe(applyBp)
|
|
3818
|
+
|
|
3819
|
+
editor.onMouseDown((e) => {
|
|
3820
|
+
if (!e?.event?.leftButton) return
|
|
3821
|
+
const target = e?.target
|
|
3822
|
+
if (!target || target.type !== GUTTER) return
|
|
3823
|
+
const line = target.position?.lineNumber
|
|
3824
|
+
if (line) dapToggleRef.current?.(path, line)
|
|
3825
|
+
})
|
|
3826
|
+
editor.onMouseMove((e) => {
|
|
3827
|
+
const target = e?.target
|
|
3828
|
+
const line = hintLineFor(target?.type, target?.position?.lineNumber, bpListOf().map((it) => it.line))
|
|
3829
|
+
setHintLine(editor, line || null)
|
|
3830
|
+
})
|
|
3831
|
+
editor.onMouseLeave(() => setHintLine(editor, null))
|
|
3832
|
+
|
|
3833
|
+
const glyphHit = (ev) => {
|
|
3834
|
+
if (!editor.getTargetAtClientPoint) return null
|
|
3835
|
+
const rect = editor.getDomNode?.()?.getBoundingClientRect?.()
|
|
3836
|
+
const layout = editor.getLayoutInfo?.()
|
|
3837
|
+
if (!rect || !layout) return null
|
|
3838
|
+
const relX = ev.clientX - rect.left
|
|
3839
|
+
if (relX < layout.glyphMarginLeft || relX > layout.glyphMarginLeft + layout.glyphMarginWidth) return null
|
|
3840
|
+
const target = editor.getTargetAtClientPoint(ev.clientX, ev.clientY)
|
|
3841
|
+
if (!target || target.type !== GUTTER) return null
|
|
3842
|
+
return target.position?.lineNumber ?? null
|
|
3843
|
+
}
|
|
3844
|
+
const onCtx = (ev) => {
|
|
3845
|
+
const line = glyphHit(ev)
|
|
3846
|
+
if (!line) return
|
|
3847
|
+
const entry = bpListOf().find((it) => it.line === line)
|
|
3848
|
+
ev.preventDefault()
|
|
3849
|
+
ev.stopPropagation()
|
|
3850
|
+
setDapBpMenu({
|
|
3851
|
+
x: ev.clientX, y: ev.clientY, path, line,
|
|
3852
|
+
hasBp: !!entry, enabled: entry ? entry.enabled !== false : true, isLogpoint: !!(entry && entry.logMessage),
|
|
3853
|
+
})
|
|
3854
|
+
}
|
|
3855
|
+
host.addEventListener('contextmenu', onCtx, true)
|
|
3856
|
+
return () => { unsubscribeBp?.(); host.removeEventListener('contextmenu', onCtx) }
|
|
3857
|
+
}
|
|
3858
|
+
/** @private @author ddj 2026年10月16号 @param host Passive body. @param path Canonical file. @param groupId Owning group id(find 快照按组隔离). @returns View-only cleanup, models remain workbench-owned. */
|
|
3859
|
+
function mountPassive(host, path, groupId) {
|
|
3860
|
+
let disposed = false, keeper = null, subscription = null, observer = null, unbindGutter = null
|
|
3771
3861
|
const focus = () => { const owner = ownerOf(benchRef.current, path); if (owner) selectGroup(owner.id, path) }
|
|
3772
3862
|
host.addEventListener('pointerdown', focus)
|
|
3773
3863
|
if (isPdfPath(path) && pdfNodes.current.has(path)) {
|
|
@@ -3792,9 +3882,13 @@ export function EditorView(props) {
|
|
|
3792
3882
|
if (!window.monaco) { host.textContent = '编辑器正在加载'; return }
|
|
3793
3883
|
const model = cached || getModel(path, result.content)
|
|
3794
3884
|
if (result.version) { revRef.current[path] = result.version; recordBaseline(scope, path, result.version) }
|
|
3795
|
-
const editor = window.monaco.editor.create(host, { model, automaticLayout: true, minimap: { enabled: false }, scrollBeyondLastLine: false
|
|
3885
|
+
const editor = window.monaco.editor.create(host, { model, automaticLayout: true, minimap: { enabled: false }, scrollBeyondLastLine: false, glyphMargin: true,
|
|
3886
|
+
// 与焦点编辑器(ensureEditor)同观感参数:字号/行高/顶部留白(side 形态 6,tab 形态 8)/装饰宽度,切分页行号位置不跳变(ddj 2026-10-16)
|
|
3887
|
+
fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)', fontSize: 16, lineHeight: 21,
|
|
3888
|
+
lineDecorationsWidth: 16, padding: { top: layoutRef.current === 'side' ? 6 : 8 } })
|
|
3889
|
+
const unbindGutter = bindBpGutter(editor, host, path)
|
|
3796
3890
|
keeper = createViewKeeper(editor)
|
|
3797
|
-
keeper.bind(scope, path, model)
|
|
3891
|
+
keeper.bind(scope, path, model, groupId ? findKeyOf(scope, groupId) : undefined)
|
|
3798
3892
|
paneEditors.current.set(path, editor)
|
|
3799
3893
|
const store = documents()
|
|
3800
3894
|
store.bind(path, model, revRef.current[path])
|
|
@@ -3802,11 +3896,15 @@ export function EditorView(props) {
|
|
|
3802
3896
|
observer = new ResizeObserver(() => editor.layout())
|
|
3803
3897
|
observer.observe(host)
|
|
3804
3898
|
}).catch((failure) => { if (!disposed) host.textContent = '文件加载失败:' + String(failure) })
|
|
3805
|
-
return () => { disposed = true; host.removeEventListener('pointerdown', focus); observer?.disconnect(); subscription?.dispose(); keeper?.dispose(); paneEditors.current.delete(path) }
|
|
3899
|
+
return () => { disposed = true; host.removeEventListener('pointerdown', focus); unbindGutter?.(); observer?.disconnect(); subscription?.dispose(); keeper?.dispose(); paneEditors.current.delete(path) }
|
|
3806
3900
|
}
|
|
3807
3901
|
const passiveOps = React.useRef(mountPassive)
|
|
3808
3902
|
passiveOps.current = mountPassive
|
|
3809
|
-
const passiveMount = React.useCallback((host, path) => passiveOps.current(host, path), [])
|
|
3903
|
+
const passiveMount = React.useCallback((host, path, groupId) => passiveOps.current(host, path, groupId), [])
|
|
3904
|
+
/** @private @author ddj 2026年10月16号 @param scope Workspace scope. @param groupId Editor group id. @returns Ctrl+F snapshot bucket key isolating find state per group. */
|
|
3905
|
+
function findKeyOf(scope, groupId) { return String(scope) + '\u0000' + String(groupId) }
|
|
3906
|
+
/** @private @author ddj 2026年10月16号 @param active Active file path. @returns Owning group id, falling back to the focused group. */
|
|
3907
|
+
function focusedGroupOf(active) { return ownerOf(benchRef.current, active)?.id ?? benchRef.current.focused }
|
|
3810
3908
|
/** @private @author ddj 2026年10月14号 @param node Split branch. @param ratio Updated branch ratio. */
|
|
3811
3909
|
function changeRatio(node, ratio) {
|
|
3812
3910
|
const replace = (current) => current === node ? { ...current, ratio } : 'group' in current ? current : { ...current, first: replace(current.first), second: replace(current.second) }
|
|
@@ -3818,7 +3916,9 @@ export function EditorView(props) {
|
|
|
3818
3916
|
if (!group) return null
|
|
3819
3917
|
const focused = id === workbench.focused
|
|
3820
3918
|
return React.createElement(EditorPane, { key: id, group, focused, dirty: dirtyMap, onSelect: selectGroup, onClose: closeTab, onMenu: paneMenu, onDrag: dragTab, consumeClick: consumeDrag },
|
|
3821
|
-
focused ? React.createElement('div', { className: 'edrv-full-slot', ref: mountFull }) : React.createElement(
|
|
3919
|
+
focused ? React.createElement('div', { className: 'edrv-full-slot', ref: mountFull }) : React.createElement(React.Fragment, null,
|
|
3920
|
+
React.createElement(GroupPathbar, { path: group.active, onFocus: () => selectGroup(id) }),
|
|
3921
|
+
React.createElement(PassiveEditor, { key: String(scope) + ':' + group.active, path: group.active, groupId: id, mount: passiveMount })))
|
|
3822
3922
|
}
|
|
3823
3923
|
/** @private @author ddj 2026年10月14号 @param node Layout subtree. @returns Recursive resizable panes. */
|
|
3824
3924
|
function renderTree(node) {
|
|
@@ -4359,10 +4459,12 @@ export function EditorView(props) {
|
|
|
4359
4459
|
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '复制安装命令', onClick: copyInstallCmd }, '复制命令'),
|
|
4360
4460
|
React.createElement('button', { className: 'edrv-side-hint-close', title: '关闭提示', 'aria-label': '关闭提示', onClick: dismissHint }, '×'))
|
|
4361
4461
|
: null
|
|
4362
|
-
|
|
4462
|
+
// 底部状态栏共用一整条:挂在 mainCol 层级(所有分组之下),不随焦点分组搬家(ddj 2026-10-16)
|
|
4463
|
+
const fullView = createPortal(React.createElement(React.Fragment, null, pathBar, sideHintEl, diskBanner(), editorArea), fullHost, 'edrv-full-editor')
|
|
4363
4464
|
const mainCol = React.createElement('div', { className: 'edrv-main-col edrv-workbench', style: { flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' } },
|
|
4364
4465
|
React.createElement('div', { className: 'edrv-workbench-tools' }, tabRow),
|
|
4365
4466
|
renderTree(workbench.tree), fullView,
|
|
4467
|
+
statusBar,
|
|
4366
4468
|
workbench.floats.map((item, layer) => React.createElement(FloatingEditor, { key: item.group, owner: String(scope), group: item.group, rect: item, layer, focused: item.group === workbench.focused, onBounds: floatBounds, onFocus: selectGroup, onReturn: returnPane }, renderGroup(item.group))),
|
|
4367
4469
|
dragPreview ? createPortal(React.createElement(React.Fragment, null,
|
|
4368
4470
|
dragPreview.rect ? React.createElement('div', { className: 'edrv-drag-overlay', style: dragPreview.rect }, React.createElement('span', { className: 'edrv-drop-label' }, dragPreview.label)) : null,
|
|
@@ -115,6 +115,9 @@ function GeneralSettings({ registry, getShortcuts }) {
|
|
|
115
115
|
const [devForm, setDevForm] = React.useState(null)
|
|
116
116
|
const [devBusy, setDevBusy] = React.useState(false)
|
|
117
117
|
const [devMessage, setDevMessage] = React.useState('')
|
|
118
|
+
const [updateState, setUpdateState] = React.useState(null) // { current, latest, hasUpdate }
|
|
119
|
+
const [updateBusy, setUpdateBusy] = React.useState(false)
|
|
120
|
+
const [updateMessage, setUpdateMessage] = React.useState('')
|
|
118
121
|
const [minW, setMinW] = React.useState(SIDEBAR_MIN_DEFAULT)
|
|
119
122
|
const [minDraft, setMinDraft] = React.useState(null) // 输入草稿(null=跟随已保存值;blur/Enter 提交)
|
|
120
123
|
const [limit, setLimit] = React.useState(EDITOR_LIMIT_DEFAULT)
|
|
@@ -198,6 +201,27 @@ function GeneralSettings({ registry, getShortcuts }) {
|
|
|
198
201
|
setDevMessage(result.restart ? '已切换为正式版,重启 DSH 后生效。' : '已关闭开发形态。')
|
|
199
202
|
}).catch((e) => setDevMessage('关闭失败:' + String(e))).finally(() => setDevBusy(false))
|
|
200
203
|
}
|
|
204
|
+
/** 检查 npm registry 是否有比当前版本更新的发布(结果落 updateState)。 */
|
|
205
|
+
const runUpdateCheck = () => {
|
|
206
|
+
setUpdateBusy(true)
|
|
207
|
+
setUpdateMessage('')
|
|
208
|
+
rpc('vscode.updateCheck', {}).then((result) => {
|
|
209
|
+
if (!result.ok) { setUpdateMessage('检查失败:' + String(result.error)); return }
|
|
210
|
+
setUpdateState({ current: result.current ?? '', latest: result.latest ?? '', hasUpdate: result.hasUpdate === true })
|
|
211
|
+
setUpdateMessage(result.hasUpdate ? '发现新版本 v' + result.latest : '已是最新版本。')
|
|
212
|
+
}).catch((e) => setUpdateMessage('检查失败:' + String(e))).finally(() => setUpdateBusy(false))
|
|
213
|
+
}
|
|
214
|
+
/** 触发官方 pluginManager「先删后装」自我更新(页面会断开,成功与否均不再回报本页)。 */
|
|
215
|
+
const runSelfUpdate = () => {
|
|
216
|
+
const target = updateState?.latest ?? ''
|
|
217
|
+
if (!window.confirm('更新到 v' + target + ':更新中插件会先卸载再安装,页面将短暂断开,完成后请刷新页面或重启 DSH。\n注意:若安装失败(如断网),插件会保持未安装状态,可在官方插件管理页重新安装恢复。确认更新?')) return
|
|
218
|
+
setUpdateBusy(true)
|
|
219
|
+
setUpdateMessage('')
|
|
220
|
+
rpc('vscode.updateSelf', {}).then((result) => {
|
|
221
|
+
if (!result.ok) { setUpdateMessage('更新失败:' + String(result.error)); setUpdateBusy(false); return }
|
|
222
|
+
setUpdateMessage('更新已开始,页面即将断开;完成后请刷新页面或重启 DSH。')
|
|
223
|
+
}).catch((e) => { setUpdateMessage('更新失败:' + String(e)); setUpdateBusy(false) })
|
|
224
|
+
}
|
|
201
225
|
/**
|
|
202
226
|
* 唤起官方快捷键弹窗(键位统一归官方机制;入口不可用时展示手动打开方式)。
|
|
203
227
|
* @author ddj 2026年09月28号
|
|
@@ -215,6 +239,17 @@ function GeneralSettings({ registry, getShortcuts }) {
|
|
|
215
239
|
React.createElement('button', { className: 'vsm-devform-close', disabled: devBusy, onClick: closeDevForm }, devBusy ? '切换中…' : '关闭开发形态'),
|
|
216
240
|
),
|
|
217
241
|
) : null
|
|
242
|
+
const updateRow = React.createElement('div', { className: 'vsm-general-row' },
|
|
243
|
+
React.createElement('span', null, '插件更新',
|
|
244
|
+
React.createElement('small', { className: 'vsm-devform-note' },
|
|
245
|
+
'当前版本 v' + (updateState?.current ?? '') + (devForm?.enabled ? ';开发形态下请走工作区同步流程' : ''))),
|
|
246
|
+
React.createElement('div', { className: 'vsm-devform-actions' },
|
|
247
|
+
updateMessage && React.createElement('small', { className: 'vsm-devform-msg' }, updateMessage),
|
|
248
|
+
updateState?.hasUpdate
|
|
249
|
+
? React.createElement('button', { disabled: updateBusy || devForm?.enabled === true, onClick: runSelfUpdate, title: devForm?.enabled ? '开发形态下请走工作区同步流程' : undefined }, updateBusy ? '更新中…' : '更新到 v' + (updateState.latest ?? ''))
|
|
250
|
+
: React.createElement('button', { disabled: updateBusy || devForm?.enabled === true, onClick: runUpdateCheck, title: devForm?.enabled ? '开发形态下请走工作区同步流程' : undefined }, updateBusy ? '检查中…' : '检查更新'),
|
|
251
|
+
),
|
|
252
|
+
)
|
|
218
253
|
return React.createElement('section', { className: 'vsm-general-page' },
|
|
219
254
|
React.createElement('h2', null, '通用'),
|
|
220
255
|
React.createElement('p', null, '配置 DSH 对话中文件链接的打开工具。工具由当前运行时自动扫描。'),
|
|
@@ -225,6 +260,7 @@ function GeneralSettings({ registry, getShortcuts }) {
|
|
|
225
260
|
React.createElement('div', { className: 'vsm-panel-body' },
|
|
226
261
|
React.createElement('label', { className: 'vsm-general-row' }, React.createElement('span', null, '文件链接使用工具'), React.createElement('select', { value: currentOption, disabled: loading || unavailable || notReady || busy || snapshot?.writable === false, onChange: (event) => save(event.target.value) }, options.map((item) => React.createElement('option', { key: item.id, value: item.id }, item.label))), loading && React.createElement('small', null, '读取中…')),
|
|
227
262
|
devFormRow,
|
|
263
|
+
updateRow,
|
|
228
264
|
),
|
|
229
265
|
),
|
|
230
266
|
React.createElement('section', { className: 'vsm-panel' },
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 可用更新主动提醒:启动延迟 + 抖动后检查 registry 新版本,
|
|
3
|
+
* 有更新且该版本未提醒过时经 toastDom 弹提示(每版本一次,localStorage 去重)。
|
|
4
|
+
* 设计:检查失败静默(不打扰);开发形态(link: 安装)下不提醒(应走工作区同步)。
|
|
5
|
+
* 作者 ddj 2026-10-16
|
|
6
|
+
*/
|
|
7
|
+
import { rpc } from './rpc.js'
|
|
8
|
+
import { toastDom } from './externalOpen.js'
|
|
9
|
+
import { PLUGIN_NAME } from '../compat.js'
|
|
10
|
+
|
|
11
|
+
/** localStorage 去重键:值为已提醒过的最新版本号。 */
|
|
12
|
+
const NOTICE_KEY = 'vsm-update-notice'
|
|
13
|
+
|
|
14
|
+
/** 启动后首次检查的延迟(毫秒)。 */
|
|
15
|
+
const NOTICE_DELAY_MS = 30000
|
|
16
|
+
|
|
17
|
+
/** 抖动上限(毫秒):避免多客户端同时请求 registry。 */
|
|
18
|
+
const NOTICE_JITTER_MS = 30000
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 读取开发形态状态(客户端侧经 RPC 查询;失败按未开启处理)。
|
|
22
|
+
* @private
|
|
23
|
+
* @author ddj 2026年10月16号
|
|
24
|
+
* @returns devForm.enabled 是否为 true
|
|
25
|
+
*/
|
|
26
|
+
async function devFormEnabled(): Promise<boolean> {
|
|
27
|
+
try {
|
|
28
|
+
const result = await rpc('vscode.devFormGet', {})
|
|
29
|
+
return result.ok === true && (result.devForm as { enabled?: boolean } | undefined)?.enabled === true
|
|
30
|
+
} catch {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 执行一次可用更新检查并在需要时弹 toast 提醒(同版本只提醒一次)。
|
|
37
|
+
* @private
|
|
38
|
+
* @author ddj 2026年10月16号
|
|
39
|
+
*/
|
|
40
|
+
async function checkAndNotify(): Promise<void> {
|
|
41
|
+
if (await devFormEnabled()) return
|
|
42
|
+
const result = await rpc('vscode.updateCheck', {})
|
|
43
|
+
if (result.ok !== true || result.hasUpdate !== true) return
|
|
44
|
+
const latest = String(result.latest ?? '')
|
|
45
|
+
if (!latest) return
|
|
46
|
+
let storage: Storage | null = null
|
|
47
|
+
try { storage = window.localStorage } catch { /* 隐私模式等场景静默降级 */ }
|
|
48
|
+
if (storage?.getItem(NOTICE_KEY) === latest) return
|
|
49
|
+
storage?.setItem(NOTICE_KEY, latest)
|
|
50
|
+
toastDom(PLUGIN_NAME + ' 有可用更新:v' + latest + '(设置页「文件与安装」可一键更新)')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 装配更新提醒:启动 30s + 0~30s 抖动后检查一次;失败静默。
|
|
55
|
+
* @public
|
|
56
|
+
* @author ddj 2026年10月16号
|
|
57
|
+
* @param ctx DSH client 上下文
|
|
58
|
+
* @param schedule 定时器注入(ctx.timeout),便于测试与生命周期统一
|
|
59
|
+
*/
|
|
60
|
+
export function installUpdateNotice(ctx: { timeout(fn: () => void, ms: number): unknown }, schedule: (fn: () => void, ms: number) => void): void {
|
|
61
|
+
schedule(() => {
|
|
62
|
+
void checkAndNotify().catch(() => { /* 检查失败静默,不打扰 */ })
|
|
63
|
+
}, NOTICE_DELAY_MS + Math.floor(Math.random() * NOTICE_JITTER_MS))
|
|
64
|
+
}
|
package/src/rpc.ts
CHANGED
|
@@ -42,6 +42,7 @@ import { unityAdd, unityInstall, unityList, unityRemove } from './unityBridge.js
|
|
|
42
42
|
import { ackPending, handoffOpen, pendingState, pollPending } from './externalHandoff.js'
|
|
43
43
|
import { buildReport } from './compat.js'
|
|
44
44
|
import { findProfileDir, readDevForm, setDevForm } from './devForm.js'
|
|
45
|
+
import { checkUpdate, startSelfUpdate } from './selfUpdate.js'
|
|
45
46
|
import { normalizeRel } from './tree.js'
|
|
46
47
|
import { baseNameOf, checkNewName, checkRenameName, isSubPath, joinRelPath, parentRelOf } from './shared/fsNames.js'
|
|
47
48
|
import { invalidateIndex, listDirCached } from './treeIndex.js'
|
|
@@ -1244,6 +1245,14 @@ export function buildHandlers(
|
|
|
1244
1245
|
return { ok: false, error: String(error) }
|
|
1245
1246
|
}
|
|
1246
1247
|
},
|
|
1248
|
+
'vscode.updateCheck': async () => {
|
|
1249
|
+
try { return await checkUpdate(ctx) }
|
|
1250
|
+
catch (error) { return { ok: false, error: String(error) } }
|
|
1251
|
+
},
|
|
1252
|
+
'vscode.updateSelf': async () => {
|
|
1253
|
+
try { return await startSelfUpdate(ctx) }
|
|
1254
|
+
catch (error) { return { ok: false, error: String(error) } }
|
|
1255
|
+
},
|
|
1247
1256
|
'edrv.integration.status': async () => {
|
|
1248
1257
|
try {
|
|
1249
1258
|
return { ok: true, ...(await shellMenuStatus(ctx, await integrationBaseUrlOf(ctx))) }
|