dsh-vscode-mode 0.15.7 → 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 +5 -3
- package/lib/client.js +377 -70
- package/lib/client.js.map +1 -1
- package/lib/index.js +210 -6
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +4 -0
- package/src/client/sessionError.ts +38 -0
- package/src/client/sidebar/panels/FileExplorer.ts +7 -1
- 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 +130 -15
- package/src/client/ui/McpSettings.ts +36 -0
- package/src/client/updateNotice.ts +64 -0
- package/src/lsp/rpc.ts +3 -2
- package/src/rpc.ts +12 -3
- package/src/selfUpdate.ts +188 -0
- package/src/shared/rpc.ts +15 -0
- package/src/svn.ts +3 -2
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')
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 会话缺失错误识别与友好文案。
|
|
3
|
+
* 跨端场景:桌面端与 Web 端各自运行独立 DSH 实例,会话列表跨实例可见,
|
|
4
|
+
* 但会话仅在一端激活;另一端发起的 edrv.* RPC 会被 host requireSession
|
|
5
|
+
* 以「会话不存在」拒绝。此处把该错误归类为终态提示(非反复重试可解),
|
|
6
|
+
* 并给出引导文案,替代裸错误卡死观感。
|
|
7
|
+
* 作者 ddj 2026年10月16号
|
|
8
|
+
*/
|
|
9
|
+
import { SESSION_MISSING_ERROR } from '../shared/rpc.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 判断错误文案是否为「会话不存在」类错误。
|
|
13
|
+
* @author ddj 2026年10月16号
|
|
14
|
+
* @param text 任意错误文本(host 业务 error、异常 message 等原始形态)
|
|
15
|
+
* @returns 命中规范文案(含拼接形态,如「会话不存在:路径」)返回 true;空值安全
|
|
16
|
+
*/
|
|
17
|
+
export function isSessionMissingError(text: unknown): boolean {
|
|
18
|
+
return String(text ?? '').includes(SESSION_MISSING_ERROR)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 会话缺失提示标题(编辑区终态面板与侧栏红条共用)。
|
|
23
|
+
* @author ddj 2026年10月16号
|
|
24
|
+
* @returns 简短标题文案
|
|
25
|
+
*/
|
|
26
|
+
export function sessionMissingTitle(): string {
|
|
27
|
+
return '会话在当前端不可用'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 会话缺失提示详情:说明成因并给出引导(去持有端查看,或在本端打开会话后重试)。
|
|
32
|
+
* @author ddj 2026年10月16号
|
|
33
|
+
* @returns 多句详情文案
|
|
34
|
+
*/
|
|
35
|
+
export function sessionMissingDetail(): string {
|
|
36
|
+
return '该会话可能正被其他 DSH 实例(桌面端 / Web 端)占用,或尚未在当前实例激活。' +
|
|
37
|
+
'请在持有该会话的一端查看,或在本端打开该会话后重试。'
|
|
38
|
+
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import React from 'react'
|
|
14
14
|
import { IconFolderClose16, IconFolderOpen16, IconRefreshOutline16, fileIconEl } from '../../ui/icons.js'
|
|
15
15
|
import { rpc } from '../../rpc.js'
|
|
16
|
+
import { isSessionMissingError, sessionMissingDetail, sessionMissingTitle } from '../../sessionError.js'
|
|
16
17
|
import { ContextMenu } from '../../ui/ContextMenu.js'
|
|
17
18
|
import { buildTreeMenu } from '../contextMenu.js'
|
|
18
19
|
import { explorerLoad, explorerSave } from '../../state/explorerCache.js'
|
|
@@ -423,7 +424,12 @@ export function FileExplorer(props) {
|
|
|
423
424
|
const updateHint = !ctx?.svn?.svnCli ? 'SVN CLI 不可用' : !ctx?.openSvnUpdate ? 'SVN 更新窗口不可用' : '更新项目(打开 SVN 更新窗口)'
|
|
424
425
|
|
|
425
426
|
const errorList = Object.entries(errors)
|
|
426
|
-
|
|
427
|
+
// 跨端会话缺失:裸错误替换为友好标题 + 详情(重试仍保留,会话在本端可用后即恢复)
|
|
428
|
+
const rawErrorText = errorList.length ? errorList[0][1] : null
|
|
429
|
+
const sessionMissing = isSessionMissingError(rawErrorText)
|
|
430
|
+
const errorText = rawErrorText === null
|
|
431
|
+
? null
|
|
432
|
+
: sessionMissing ? sessionMissingTitle() + ':' + sessionMissingDetail() : rawErrorText
|
|
427
433
|
|
|
428
434
|
return React.createElement('div', { className: 'edrv-side-panel', onContextMenu: onPanelContext },
|
|
429
435
|
React.createElement('div', { className: 'edrv-side-head' },
|
|
@@ -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'
|
|
@@ -118,6 +118,7 @@ import { buildBpMenu } from '../dap/bpMenu.js'
|
|
|
118
118
|
import { clampToolbarPosition, loadToolbarPosition, saveToolbarPosition } from '../dap/toolbarDrag.js'
|
|
119
119
|
import { editorModelPathOf } from '../dap/modelPath.js'
|
|
120
120
|
import { dapTrace } from '../dap/trace.js'
|
|
121
|
+
import { isSessionMissingError, sessionMissingDetail, sessionMissingTitle } from '../sessionError.js'
|
|
121
122
|
|
|
122
123
|
/**
|
|
123
124
|
* 只在 Lua 注解类型槽开启注释自动建议;跨文件/离开类型槽立即恢复默认。
|
|
@@ -1897,6 +1898,37 @@ export function EditorView(props) {
|
|
|
1897
1898
|
loadContent(active, sessionId)
|
|
1898
1899
|
}, [active, sessionId])
|
|
1899
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
|
+
|
|
1900
1932
|
// 页签右键菜单的 Esc 关闭由 ContextMenu 组件自己处理(capture 监听),此处不再重复挂监听
|
|
1901
1933
|
|
|
1902
1934
|
React.useEffect(() => {
|
|
@@ -2190,7 +2222,7 @@ export function EditorView(props) {
|
|
|
2190
2222
|
if (!monaco || !editorRef.current || !active || !contentReady) return
|
|
2191
2223
|
const ed = editorRef.current
|
|
2192
2224
|
const model = getModel(active, content)
|
|
2193
|
-
viewKeeperRef.current?.bind(String(scope), active, model)
|
|
2225
|
+
viewKeeperRef.current?.bind(String(scope), active, model, findKeyOf(scope, focusedGroupOf(active)))
|
|
2194
2226
|
syncTypeSuggest(ed, monaco)
|
|
2195
2227
|
setLoadStage((prev) => ({ progress: Math.max(96, prev.progress), message: '创建编辑器视图…' }))
|
|
2196
2228
|
}, [monaco, scope, active, content, contentReady, mdPreviewing, svnDiff])
|
|
@@ -2290,8 +2322,9 @@ export function EditorView(props) {
|
|
|
2290
2322
|
language: 'plaintext',
|
|
2291
2323
|
theme: themeNameOf(),
|
|
2292
2324
|
fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)',
|
|
2293
|
-
|
|
2294
|
-
|
|
2325
|
+
// 16px/21px:与 md 预览渲染态同字号,切分页/切组时观感不再跳变(ddj 2026-10-16)
|
|
2326
|
+
fontSize: 16,
|
|
2327
|
+
lineHeight: 21,
|
|
2295
2328
|
minimap: { enabled: !side, scale: 1 },
|
|
2296
2329
|
glyphMargin: true,
|
|
2297
2330
|
lineDecorationsWidth: 16,
|
|
@@ -3703,16 +3736,21 @@ export function EditorView(props) {
|
|
|
3703
3736
|
saveViewState()
|
|
3704
3737
|
recordNav()
|
|
3705
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)
|
|
3706
3742
|
setWorkbench(next)
|
|
3707
3743
|
setPlanActive(null)
|
|
3708
|
-
setDiskActive(
|
|
3744
|
+
setDiskActive(nextActive)
|
|
3709
3745
|
}
|
|
3710
3746
|
/** @private @author ddj 2026年10月14号 @param next Atomic layout. Accept ownership without close/save side effects. */
|
|
3711
3747
|
function acceptLayout(next) {
|
|
3712
3748
|
saveViewState()
|
|
3749
|
+
const nextActive = next.groups.find((group) => group.id === next.focused)?.active ?? null
|
|
3750
|
+
if (nextActive && nextActive !== activeRef.current) resetContentForSwitch(nextActive)
|
|
3713
3751
|
setWorkbench(next)
|
|
3714
3752
|
setPlanActive(null)
|
|
3715
|
-
setDiskActive(
|
|
3753
|
+
setDiskActive(nextActive)
|
|
3716
3754
|
}
|
|
3717
3755
|
/** @private @author ddj 2026年10月14号 @param path File. @param edge Split direction. Move the file into a new group. */
|
|
3718
3756
|
function splitTab(path, edge) {
|
|
@@ -3764,9 +3802,62 @@ export function EditorView(props) {
|
|
|
3764
3802
|
}
|
|
3765
3803
|
/** @private @author ddj 2026年10月14号 @param host Group body. Keep the full editor subtree stable while changing group/window ownership. */
|
|
3766
3804
|
function mountFull(host) { if (host && fullHost.parentElement !== host) { host.appendChild(fullHost); editorRef.current?.layout() } }
|
|
3767
|
-
/** @private @author ddj 2026年10月
|
|
3768
|
-
function
|
|
3769
|
-
|
|
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
|
|
3770
3861
|
const focus = () => { const owner = ownerOf(benchRef.current, path); if (owner) selectGroup(owner.id, path) }
|
|
3771
3862
|
host.addEventListener('pointerdown', focus)
|
|
3772
3863
|
if (isPdfPath(path) && pdfNodes.current.has(path)) {
|
|
@@ -3791,9 +3882,13 @@ export function EditorView(props) {
|
|
|
3791
3882
|
if (!window.monaco) { host.textContent = '编辑器正在加载'; return }
|
|
3792
3883
|
const model = cached || getModel(path, result.content)
|
|
3793
3884
|
if (result.version) { revRef.current[path] = result.version; recordBaseline(scope, path, result.version) }
|
|
3794
|
-
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)
|
|
3795
3890
|
keeper = createViewKeeper(editor)
|
|
3796
|
-
keeper.bind(scope, path, model)
|
|
3891
|
+
keeper.bind(scope, path, model, groupId ? findKeyOf(scope, groupId) : undefined)
|
|
3797
3892
|
paneEditors.current.set(path, editor)
|
|
3798
3893
|
const store = documents()
|
|
3799
3894
|
store.bind(path, model, revRef.current[path])
|
|
@@ -3801,11 +3896,15 @@ export function EditorView(props) {
|
|
|
3801
3896
|
observer = new ResizeObserver(() => editor.layout())
|
|
3802
3897
|
observer.observe(host)
|
|
3803
3898
|
}).catch((failure) => { if (!disposed) host.textContent = '文件加载失败:' + String(failure) })
|
|
3804
|
-
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) }
|
|
3805
3900
|
}
|
|
3806
3901
|
const passiveOps = React.useRef(mountPassive)
|
|
3807
3902
|
passiveOps.current = mountPassive
|
|
3808
|
-
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 }
|
|
3809
3908
|
/** @private @author ddj 2026年10月14号 @param node Split branch. @param ratio Updated branch ratio. */
|
|
3810
3909
|
function changeRatio(node, ratio) {
|
|
3811
3910
|
const replace = (current) => current === node ? { ...current, ratio } : 'group' in current ? current : { ...current, first: replace(current.first), second: replace(current.second) }
|
|
@@ -3817,7 +3916,9 @@ export function EditorView(props) {
|
|
|
3817
3916
|
if (!group) return null
|
|
3818
3917
|
const focused = id === workbench.focused
|
|
3819
3918
|
return React.createElement(EditorPane, { key: id, group, focused, dirty: dirtyMap, onSelect: selectGroup, onClose: closeTab, onMenu: paneMenu, onDrag: dragTab, consumeClick: consumeDrag },
|
|
3820
|
-
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 })))
|
|
3821
3922
|
}
|
|
3822
3923
|
/** @private @author ddj 2026年10月14号 @param node Layout subtree. @returns Recursive resizable panes. */
|
|
3823
3924
|
function renderTree(node) {
|
|
@@ -4024,6 +4125,18 @@ export function EditorView(props) {
|
|
|
4024
4125
|
body = React.createElement('div', { className: 'edrv-empty' },
|
|
4025
4126
|
React.createElement('div', null, '暂无打开的文件'),
|
|
4026
4127
|
React.createElement('div', { style: { fontSize: 12 } }, '使用右上搜索框 (' + (chordOf('edrv.quickOpen') ?? 'Ctrl+P') + ') 打开工作区文件;' + (chordOf('edrv.showCommands') ?? 'Ctrl+Shift+P') + ' 打开命令栏;agent 修改文件后顶部会出现差异角标'))
|
|
4128
|
+
} else if (content === null && loadError && isSessionMissingError(loadError)) {
|
|
4129
|
+
// 跨端会话缺失:终态提示而非加载失败卡死(会话仅在一端激活,本端 RPC 均被拒)
|
|
4130
|
+
body = React.createElement('div', { className: 'edrv-empty edrv-loading', 'aria-live': 'polite' },
|
|
4131
|
+
React.createElement('div', { className: 'edrv-loading-title' }, sessionMissingTitle()),
|
|
4132
|
+
React.createElement('div', { style: { fontSize: 12, maxWidth: 420, lineHeight: 1.6 } }, sessionMissingDetail()),
|
|
4133
|
+
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', onClick: () => {
|
|
4134
|
+
setLoadError(null)
|
|
4135
|
+
setContent(null)
|
|
4136
|
+
setContentPath(null)
|
|
4137
|
+
setLoadStage({ progress: 10, message: '重新读取文件…' })
|
|
4138
|
+
loadContent(active, sessionId)
|
|
4139
|
+
} }, '重试'))
|
|
4027
4140
|
} else if (content === null && loadError) {
|
|
4028
4141
|
body = loadingBody('文件加载失败:' + loadError, 0, () => {
|
|
4029
4142
|
setLoadError(null)
|
|
@@ -4346,10 +4459,12 @@ export function EditorView(props) {
|
|
|
4346
4459
|
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '复制安装命令', onClick: copyInstallCmd }, '复制命令'),
|
|
4347
4460
|
React.createElement('button', { className: 'edrv-side-hint-close', title: '关闭提示', 'aria-label': '关闭提示', onClick: dismissHint }, '×'))
|
|
4348
4461
|
: null
|
|
4349
|
-
|
|
4462
|
+
// 底部状态栏共用一整条:挂在 mainCol 层级(所有分组之下),不随焦点分组搬家(ddj 2026-10-16)
|
|
4463
|
+
const fullView = createPortal(React.createElement(React.Fragment, null, pathBar, sideHintEl, diskBanner(), editorArea), fullHost, 'edrv-full-editor')
|
|
4350
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' } },
|
|
4351
4465
|
React.createElement('div', { className: 'edrv-workbench-tools' }, tabRow),
|
|
4352
4466
|
renderTree(workbench.tree), fullView,
|
|
4467
|
+
statusBar,
|
|
4353
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))),
|
|
4354
4469
|
dragPreview ? createPortal(React.createElement(React.Fragment, null,
|
|
4355
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/lsp/rpc.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { onRuntimeProvisioned, envInstallStates } from './dotnetProvision.js'
|
|
|
15
15
|
import { envRequirementsFor, installRequirement } from './envRequirements.js'
|
|
16
16
|
import type { LspManager } from './manager.js'
|
|
17
17
|
import type { RpcHandlerMap } from '../shared/rpc.js'
|
|
18
|
+
import { SESSION_MISSING_ERROR, SESSION_NO_WORKSPACE_ERROR } from '../shared/rpc.js'
|
|
18
19
|
import type { LspLocation, LspServerStatus } from '../shared/lsp.js'
|
|
19
20
|
import { installFromMarket, installVsixFile, listInstalled, marketGet, marketSearch, uninstall, updateExtension, type ExtInfo } from './extmgr.js'
|
|
20
21
|
import { toWorkspacePath } from './uri.js'
|
|
@@ -152,9 +153,9 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
|
|
|
152
153
|
/** 解析会话与工作区根(失败 → null + error)。 */
|
|
153
154
|
const rootOf = async (sessionId: string | undefined): Promise<{ root: string; sessionId: string } | { err: string }> => {
|
|
154
155
|
const session = sessionOf(ctx, sessionId)
|
|
155
|
-
if (!session) return { err:
|
|
156
|
+
if (!session) return { err: SESSION_MISSING_ERROR }
|
|
156
157
|
const cwd = cwdOf(session)
|
|
157
|
-
if (!cwd) return { err:
|
|
158
|
+
if (!cwd) return { err: SESSION_NO_WORKSPACE_ERROR }
|
|
158
159
|
try {
|
|
159
160
|
const root = await searchRoot(ctx, session)
|
|
160
161
|
const sid = typeof session.id === 'string' && session.id ? session.id : sessionId ?? ''
|