dsh-vscode-mode 0.15.3 → 0.15.5
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 +8 -3
- package/lib/client.js +535 -388
- package/lib/client.js.map +1 -1
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/compatSummary.ts +77 -0
- package/src/client/externalOpen.ts +21 -3
- package/src/client/index.ts +14 -10
- package/src/client/monaco/lsp/lspClient.ts +26 -5
- package/src/client/monaco/lsp/providers.ts +3 -1
- package/src/client/state/floatingWindow.ts +50 -0
- package/src/client/styles/editor.css +14 -1
- package/src/client/ui/EditorView.ts +26 -30
- package/src/client/ui/FloatingEditor.ts +16 -70
- package/src/client/ui/FloatingWindow.ts +168 -0
- package/src/client/ui/McpSettings.ts +1 -1
- package/src/client/ui/SvnAiPlanDialog.ts +4 -4
- package/src/client/ui/SvnLogDialog.ts +94 -125
- package/src/client/ui/SvnLogStats.ts +4 -5
- package/src/client/ui/SvnPatchDialog.ts +4 -3
- package/src/client/ui/SvnSumDialog.ts +4 -3
- package/src/client/ui/SvnUpdateDialog.ts +4 -5
- package/src/svn.ts +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.5",
|
|
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": [
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { CompatAdapter } from '../shared/compat.js'
|
|
2
|
+
import { SIDEBAR_PLUGIN } from './compat.js'
|
|
3
|
+
|
|
4
|
+
/** Runtime installation facts, separate from the DSH version and user intent. */
|
|
5
|
+
export interface ClientCompatState {
|
|
6
|
+
settingsService: string
|
|
7
|
+
settingsReady: boolean
|
|
8
|
+
openPathReady: boolean
|
|
9
|
+
remoteReady: boolean
|
|
10
|
+
sidebarOpener: boolean
|
|
11
|
+
legacySidebar: boolean
|
|
12
|
+
officialSidebar: boolean
|
|
13
|
+
fileClaim: boolean
|
|
14
|
+
claimWanted: boolean
|
|
15
|
+
selected: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Classify the API generation without importing the host's node-only version probe.
|
|
20
|
+
* @private @author ddj 2026年10月13号
|
|
21
|
+
* @param input Running DSH version from the compat RPC.
|
|
22
|
+
* @param patch First supported 0.1.x patch (alpha.1).
|
|
23
|
+
* @returns Whether that API generation is expected; undefined for unknown versions.
|
|
24
|
+
*/
|
|
25
|
+
function apiExpected(input: string, patch: number): boolean | undefined {
|
|
26
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*))?(?:\+[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?$/.exec(input.trim())
|
|
27
|
+
if (!match) return undefined
|
|
28
|
+
const major = Number(match[1])
|
|
29
|
+
const minor = Number(match[2])
|
|
30
|
+
const current = Number(match[3])
|
|
31
|
+
if (major !== 0) return major > 0
|
|
32
|
+
if (minor !== 1) return minor > 1
|
|
33
|
+
if (current !== patch) return current > patch
|
|
34
|
+
const pre = match[4]
|
|
35
|
+
if (!pre) return true
|
|
36
|
+
const parts = pre.split('.')
|
|
37
|
+
if (/^\d+$/.test(parts[0])) return false
|
|
38
|
+
if (parts[0] !== 'alpha') return parts[0] > 'alpha'
|
|
39
|
+
return parts.length > 1 && (/^\d+$/.test(parts[1]) ? Number(parts[1]) >= 1 : true)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build only applicable diagnostics; explicit legacy choices still expose missing capabilities.
|
|
44
|
+
* @public @author ddj 2026年10月13号
|
|
45
|
+
* @param state Actual client installation facts and user selection.
|
|
46
|
+
* @param version Running DSH version; unknown versions retain conservative diagnostics.
|
|
47
|
+
* @returns Visible adapters, without changing routing or installation.
|
|
48
|
+
*/
|
|
49
|
+
export function clientSummary(state: ClientCompatState, version = ''): CompatAdapter[] {
|
|
50
|
+
const remoteExpected = apiExpected(version, 3)
|
|
51
|
+
const officialExpected = apiExpected(version, 5)
|
|
52
|
+
const official = state.officialSidebar
|
|
53
|
+
const legacyChosen = state.selected === SIDEBAR_PLUGIN
|
|
54
|
+
const items: CompatAdapter[] = [{
|
|
55
|
+
name: '设置桥', active: state.settingsReady,
|
|
56
|
+
note: state.settingsReady ? '使用 ' + state.settingsService + ' 桥' : '未绑定设置服务(持久化不可用,晚到重试窗口内自愈)',
|
|
57
|
+
}]
|
|
58
|
+
if (remoteExpected !== true && !state.remoteReady && !official) {
|
|
59
|
+
items.push({ name: '文件打开路由(workspaces.openPath)', active: state.openPathReady, note: '旧版对话文件链接路由,失败回退系统打开' })
|
|
60
|
+
}
|
|
61
|
+
if (remoteExpected !== false || state.remoteReady) {
|
|
62
|
+
items.push({ name: '会话文件链接路由(remote.session.openWorkspacePath)', active: state.remoteReady, note: state.remoteReady ? '会话文件链接走插件打开器,失败回退系统打开' : '适用路由未安装(服务缺失或补丁失败)' })
|
|
63
|
+
}
|
|
64
|
+
if (legacyChosen || state.sidebarOpener || (!official && officialExpected !== true)) {
|
|
65
|
+
items.push({ name: '侧边栏打开器(' + SIDEBAR_PLUGIN + ')', active: state.sidebarOpener, note: state.sidebarOpener ? '已注册(优先级 80)' : '未检测到侧边栏打开能力' })
|
|
66
|
+
}
|
|
67
|
+
if (official || officialExpected !== false) {
|
|
68
|
+
items.push({ name: '侧边栏编辑区(官方 Sidebar)', active: official, note: official ? '编辑区=官方右侧 Sidebar Tab(对话+编辑同屏)' : '官方侧边栏服务未探测到' })
|
|
69
|
+
}
|
|
70
|
+
if (!official && (state.legacySidebar || officialExpected !== true)) {
|
|
71
|
+
items.push({ name: '侧边栏编辑区(' + SIDEBAR_PLUGIN + ',归档)', active: state.legacySidebar, note: state.legacySidebar ? '编辑区=旧版侧边栏 Tab' : '旧版侧边栏回退能力未检测到' })
|
|
72
|
+
}
|
|
73
|
+
if (state.fileClaim || (state.claimWanted && (official || officialExpected !== false))) {
|
|
74
|
+
items.push({ name: '文件链接官方认领(dsh-resource://file)', active: state.fileClaim, note: state.fileClaim ? '聊天文件链接转发进单一编辑器页签' : '官方文件链接认领未安装' })
|
|
75
|
+
}
|
|
76
|
+
return items
|
|
77
|
+
}
|
|
@@ -15,6 +15,12 @@ import { log } from './log.js'
|
|
|
15
15
|
const TOAST_MS = 5000
|
|
16
16
|
/** 深链移交轮询周期(兼活跃心跳)。 */
|
|
17
17
|
const HANDOFF_POLL_MS = 3000
|
|
18
|
+
/**
|
|
19
|
+
* 页面隐藏时的降频倍数(3s × 5 = 15s)。
|
|
20
|
+
* 必须仍短于 host 租约(src/externalHandoff.ts 的 LEASE_MS = 30s):心跳一旦超期,
|
|
21
|
+
* host 判定该页面已死,launcher 会重复开新页——比多几次 RPC 更糟。
|
|
22
|
+
*/
|
|
23
|
+
const HIDDEN_POLL_EVERY = 5
|
|
18
24
|
|
|
19
25
|
/** 深链装配选项(全部可注入便于测试;缺省读浏览器全局)。 */
|
|
20
26
|
export interface ExtOpenOptions {
|
|
@@ -52,9 +58,10 @@ export function setupExtOpen(ctx: unknown, options: ExtOpenOptions = {}): () =>
|
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
/**
|
|
55
|
-
*
|
|
61
|
+
* 深链移交轮询:可见时每 3s 向 host 领取待打开请求(兼活跃心跳,launcher 据此决定是否开新页);
|
|
62
|
+
* 页面隐藏时降到 15s 一次——无人看页面时移交不必保持 3s 精度,但心跳不能停(超期会让 host 判页面已死)。
|
|
56
63
|
* 页面级单例(window 标记防 HMR 重复);领取到即执行打开规则。
|
|
57
|
-
* @author ddj 2026年09月08号
|
|
64
|
+
* @author ddj 2026年09月08号 / 2026年10月15号
|
|
58
65
|
* @param ctx 客户端根上下文
|
|
59
66
|
*/
|
|
60
67
|
function startPresencePoll(ctx: unknown): () => void {
|
|
@@ -65,6 +72,17 @@ function startPresencePoll(ctx: unknown): () => void {
|
|
|
65
72
|
let disposed = false
|
|
66
73
|
let polling = false
|
|
67
74
|
let executing = false
|
|
75
|
+
let ticks = 0
|
|
76
|
+
/**
|
|
77
|
+
* 页面隐藏时按倍数跳轮;可见时每次都跑并清零计数。
|
|
78
|
+
* @author ddj 2026年10月15号
|
|
79
|
+
* @returns 是否跳过本轮
|
|
80
|
+
*/
|
|
81
|
+
function hiddenSkip(): boolean {
|
|
82
|
+
if (typeof document === 'undefined' || document.visibilityState !== 'hidden') { ticks = 0; return false }
|
|
83
|
+
ticks += 1
|
|
84
|
+
return ticks % HIDDEN_POLL_EVERY !== 0
|
|
85
|
+
}
|
|
68
86
|
/** @private @author ddj 2026年09月28号 Open all requested paths before acknowledging this exact lease. */
|
|
69
87
|
async function execute(open: { paths: string[]; line?: number; column?: number; token: string; leaseId: string }): Promise<void> {
|
|
70
88
|
executing = true
|
|
@@ -82,7 +100,7 @@ function startPresencePoll(ctx: unknown): () => void {
|
|
|
82
100
|
if (!disposed && result.ok && result.open && !executing) void execute(result.open).catch(() => {})
|
|
83
101
|
} finally { polling = false }
|
|
84
102
|
}
|
|
85
|
-
const timer = window.setInterval(() => { void tick().catch(() => {}) }, HANDOFF_POLL_MS)
|
|
103
|
+
const timer = window.setInterval(() => { if (hiddenSkip()) return; void tick().catch(() => {}) }, HANDOFF_POLL_MS)
|
|
86
104
|
/** @private @author ddj 2026年09月28号 Stop heartbeats and fail outstanding editor waits on plugin disposal. */
|
|
87
105
|
function dispose(): void {
|
|
88
106
|
disposed = true
|
package/src/client/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { openEditorView } from './events.js'
|
|
|
32
32
|
import { setupExtOpen } from './externalOpen.js'
|
|
33
33
|
import { SettingsContext } from './settingsContext.js'
|
|
34
34
|
import { SIDEBAR_PLUGIN, registerSlotSafely, settingsBridge } from './compat.js'
|
|
35
|
+
import { clientSummary } from './compatSummary.js'
|
|
35
36
|
import { detectSidebarService, hasSideRoute, installSideEditor, routeSideEditor, setEnsureSideEditor, SIDEBAR_INSTALL_CMD } from './sidebarBridge.js'
|
|
36
37
|
import { detectOfficial, installOfficial, registerOfficialFileClaim, OFFICIAL_TAB_KIND, OFFICIAL_TAB_TITLE, isEditorTabActive, markEditorActive, restoreEditorTab, buildFileAddress, readSavedTab, setEditorTabScope } from './officialSidebar.js'
|
|
37
38
|
import { setNativeCsv, setNativeOpenHandler, resetNativeOpen } from './nativeOpenStore.js'
|
|
@@ -264,16 +265,19 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
264
265
|
/** 「添加到对话」动作集:编辑区/Tab 右键菜单注入文件引用与代码块(conversation 服务缺失时各动作安全降级)。 */
|
|
265
266
|
const addToConversation = createAddToConversation(ctx)
|
|
266
267
|
|
|
267
|
-
/**
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
268
|
+
/**
|
|
269
|
+
* Build applicable client diagnostics from live installation state.
|
|
270
|
+
* @private @author ddj 2026年10月13号
|
|
271
|
+
* @param version Running DSH version supplied by the compat RPC.
|
|
272
|
+
* @returns Applicable adapters; routing behavior is unchanged.
|
|
273
|
+
*/
|
|
274
|
+
const compatSummary = (version = ''): CompatAdapter[] => clientSummary({
|
|
275
|
+
settingsService: bridge.service(), settingsReady: bridge.scope() !== undefined,
|
|
276
|
+
openPathReady: Boolean(workspaces?.openPath), remoteReady: remoteOpenInstalled,
|
|
277
|
+
sidebarOpener: registry.get(SIDEBAR_PLUGIN) !== undefined,
|
|
278
|
+
legacySidebar: sideService !== undefined, officialSidebar: officialService !== undefined,
|
|
279
|
+
fileClaim: claimDisposer !== null, claimWanted: shouldClaimFiles(selected), selected,
|
|
280
|
+
}, version)
|
|
277
281
|
|
|
278
282
|
ctx.provide('fileOpeners', registry)
|
|
279
283
|
ctx.effect(() => registry.register(vscodeOpener()), 'vscode-mode: file opener')
|
|
@@ -108,25 +108,46 @@ function nextDocVer(path) {
|
|
|
108
108
|
/**
|
|
109
109
|
* 立即同步某文档到 host(首次打开/查询前保证服务器已认识该文档)。
|
|
110
110
|
* sessionId 缺失时仍发起请求:host 对唯一活跃会话有兜底解析,弃发会白白丢失文档归属。
|
|
111
|
-
* @author ddj 2026年08月27号 / 2026年09月11号
|
|
111
|
+
* @author ddj 2026年08月27号 / 2026年09月11号 / 2026年10月15号
|
|
112
112
|
* @param path 工作区相对路径
|
|
113
|
-
* @param text
|
|
113
|
+
* @param text 文档全文;传函数表示**发送前才求值**(击键路径用它,避免为被去抖丢弃的中间态取全文)
|
|
114
114
|
* @param immediate true 立即发送,false 去抖
|
|
115
115
|
*/
|
|
116
116
|
export async function syncDoc(path, text, immediate = false) {
|
|
117
|
-
if (!path || typeof text !== 'string') return
|
|
117
|
+
if (!path || (typeof text !== 'string' && typeof text !== 'function')) return
|
|
118
118
|
if (immediate) {
|
|
119
119
|
clearPending(path)
|
|
120
|
-
|
|
120
|
+
const value = resolveText(text)
|
|
121
|
+
if (value === null) return
|
|
122
|
+
await rpc('edrv.lsp.sync', { sessionId, path, text: value, version: nextDocVer(path) }).catch(() => {})
|
|
121
123
|
return
|
|
122
124
|
}
|
|
123
125
|
clearPending(path)
|
|
124
126
|
syncTimers.set(path, setTimeout(() => {
|
|
125
127
|
syncTimers.delete(path)
|
|
126
|
-
|
|
128
|
+
const value = resolveText(text)
|
|
129
|
+
if (value === null) return
|
|
130
|
+
rpc('edrv.lsp.sync', { sessionId, path, text: value, version: nextDocVer(path) }).catch(() => {})
|
|
127
131
|
}, SYNC_DEBOUNCE_MS))
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
/**
|
|
135
|
+
* 取本次要同步的文本:字符串原样返回,函数形态在发送前求值。
|
|
136
|
+
* 取值抛错(model 已销毁)时返回 null 表示**放弃本次同步**——不能退回空串,
|
|
137
|
+
* 那会把 host 侧文档内容清掉,比漏一次同步严重得多(关闭路径由 closeDoc 负责)。
|
|
138
|
+
* @author ddj 2026年10月15号
|
|
139
|
+
* @param text 字符串或惰性取值函数
|
|
140
|
+
* @returns 待发送文本;无法取值时为 null
|
|
141
|
+
*/
|
|
142
|
+
function resolveText(text) {
|
|
143
|
+
if (typeof text === 'string') return text
|
|
144
|
+
try {
|
|
145
|
+
return String(text())
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
130
151
|
function clearPending(path) {
|
|
131
152
|
const timer = syncTimers.get(path)
|
|
132
153
|
if (timer) { clearTimeout(timer); syncTimers.delete(path) }
|
|
@@ -95,7 +95,9 @@ export function registerLspProviders(monaco) {
|
|
|
95
95
|
if (!model || model.uri.scheme !== 'edrv') return
|
|
96
96
|
const path = pathOfModel(model)
|
|
97
97
|
if (!path) return
|
|
98
|
-
|
|
98
|
+
// 击键路径传惰性取值:去抖窗口内的中间态会被覆盖丢弃,事先取全文纯属白付(大文件尤甚)。
|
|
99
|
+
// model 已销毁时 resolveText 返回 null 放弃本次同步,不会拿空内容覆盖 host 侧文档。
|
|
100
|
+
const contentDisposer = model.onDidChangeContent(() => syncDoc(path, () => model.getValue()))
|
|
99
101
|
const disposeDisposer = model.onWillDispose(() => {
|
|
100
102
|
closeDoc(path)
|
|
101
103
|
contentDisposer.dispose()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Shared nonmodal geometry and bounded stacking. @author ddj 2026年10月14号 */
|
|
2
|
+
import { clampFloat } from './editorLayout.js'
|
|
3
|
+
import type { FloatRect } from './editorLayout.js'
|
|
4
|
+
|
|
5
|
+
const windows: string[] = []
|
|
6
|
+
const listeners = new Set<() => void>()
|
|
7
|
+
let version = 0
|
|
8
|
+
|
|
9
|
+
/** @private @author ddj 2026年10月14号 Notify every mounted window after an order change. */
|
|
10
|
+
function notify(): void { version += 1; for (const listener of listeners) listener() }
|
|
11
|
+
/** @public @author ddj 2026年10月14号 @param listener Order observer. @returns Unsubscribe callback. */
|
|
12
|
+
export function watchFloats(listener: () => void): () => void {
|
|
13
|
+
listeners.add(listener)
|
|
14
|
+
return () => { listeners.delete(listener) }
|
|
15
|
+
}
|
|
16
|
+
/** @public @author ddj 2026年10月14号 @returns Stable external-store revision. */
|
|
17
|
+
export function floatVersion(): number { return version }
|
|
18
|
+
/** @public @author ddj 2026年10月14号 @param id Window identity. Bring forward without exceeding modal layers. */
|
|
19
|
+
export function raiseFloat(id: string): void {
|
|
20
|
+
const index = windows.indexOf(id)
|
|
21
|
+
if (index === windows.length - 1 && index >= 0) return
|
|
22
|
+
if (index >= 0) windows.splice(index, 1)
|
|
23
|
+
windows.push(id)
|
|
24
|
+
notify()
|
|
25
|
+
}
|
|
26
|
+
/** @public @author ddj 2026年10月14号 @param id Unmounted window identity. */
|
|
27
|
+
export function dropFloat(id: string): void {
|
|
28
|
+
const index = windows.indexOf(id)
|
|
29
|
+
if (index < 0) return
|
|
30
|
+
windows.splice(index, 1)
|
|
31
|
+
notify()
|
|
32
|
+
}
|
|
33
|
+
/** @public @author ddj 2026年10月14号 @param id Window identity. @returns Layer below menus/modals (190+). */
|
|
34
|
+
export function floatLayer(id: string): number {
|
|
35
|
+
return 60 + Math.floor(120 * Math.max(0, windows.indexOf(id)) / Math.max(1, windows.length))
|
|
36
|
+
}
|
|
37
|
+
/** @public @author ddj 2026年10月14号 @param width Preferred width. @param height Preferred height. @param vw Viewport width. @param vh Viewport height. @returns Centered, reachable bounds. */
|
|
38
|
+
export function initialFloat(width: number, height: number, vw: number, vh: number): FloatRect {
|
|
39
|
+
return clampFloat({ width, height, left: (vw - width) / 2, top: (vh - height) / 2 }, vw, vh)
|
|
40
|
+
}
|
|
41
|
+
/** @public @author ddj 2026年10月14号 @param rect Gesture origin. @param dx Horizontal delta. @param dy Vertical delta. @param sizing Resize instead of move. @param vw Viewport width. @param vh Viewport height. @returns Clamped next bounds. */
|
|
42
|
+
export function moveFloat(rect: FloatRect, dx: number, dy: number, sizing: boolean, vw: number, vh: number): FloatRect {
|
|
43
|
+
const next = sizing ? { ...rect, width: rect.width + dx, height: rect.height + dy }
|
|
44
|
+
: { ...rect, left: rect.left + dx, top: rect.top + dy }
|
|
45
|
+
return clampFloat(next, vw, vh)
|
|
46
|
+
}
|
|
47
|
+
/** @public @author ddj 2026年10月14号 @param target Key target. @returns Whether native text/control navigation owns the key. */
|
|
48
|
+
export function inputOwnsKey(target: EventTarget | null): boolean {
|
|
49
|
+
return target instanceof Element && !!target.closest('input, textarea, select, [contenteditable="true"], .monaco-editor')
|
|
50
|
+
}
|
|
@@ -994,7 +994,20 @@
|
|
|
994
994
|
[data-edrv-view] .edrv-split-handle:hover, [data-edrv-view] .edrv-split-handle:focus-visible { background: var(--dsw-alias-state-info-primary, #4d6bfe); }
|
|
995
995
|
[data-edrv-view].edrv-floating { position: fixed; display: flex; flex-direction: column; overflow: hidden; box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l2, #cdd6da); background: var(--dsw-alias-bg-base, #fff); color: var(--dsw-alias-label-primary, #1f2933); box-shadow: 0 8px 28px rgba(0,0,0,.22); }
|
|
996
996
|
[data-edrv-view] .edrv-floating-title { display: flex; align-items: center; gap: 8px; flex: 0 0 32px; padding: 0 8px; background: var(--dsw-alias-bg-layer-1, #f7f9fa); cursor: move; touch-action: none; user-select: none; }
|
|
997
|
-
[data-edrv-view] .edrv-floating-title > span { flex: 1; }
|
|
997
|
+
[data-edrv-view] .edrv-floating-title > span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
998
|
+
[data-edrv-view] .edrv-floating-content { display: flex; flex-direction: column; flex: 1; min-width: 0; min-height: 0; overflow: auto; box-sizing: border-box; }
|
|
999
|
+
[data-edrv-view] .edrv-floating-content.edrv-svnlog-dialog { padding: 12px; overflow: auto; }
|
|
1000
|
+
[data-edrv-view] .edrv-floating-content.edrv-svn-patch-dialog,
|
|
1001
|
+
[data-edrv-view] .edrv-floating-content.edrv-svn-sum-dialog,
|
|
1002
|
+
[data-edrv-view] .edrv-floating-content.edrv-svnplan-dialog { padding: 12px; max-width: none; gap: 8px; }
|
|
1003
|
+
[data-edrv-view] .edrv-floating-content.edrv-svnlog-stats { max-height: none; }
|
|
1004
|
+
[data-edrv-view] .edrv-floating-content .edrv-svn-patch-pre,
|
|
1005
|
+
[data-edrv-view] .edrv-floating-content .edrv-svn-sum-list { flex: 1; min-height: 0; max-height: none; }
|
|
1006
|
+
[data-edrv-view] .edrv-floating-content .edrv-svnlog-filter,
|
|
1007
|
+
[data-edrv-view] .edrv-floating-content .edrv-svn-sum-form { flex-wrap: wrap; }
|
|
1008
|
+
[data-edrv-view] .edrv-floating-content > .edrv-svnlog-head,
|
|
1009
|
+
[data-edrv-view] .edrv-floating-content > .edrv-svnlog-filter,
|
|
1010
|
+
[data-edrv-view] .edrv-floating-content > .edrv-svnlog-foot { flex-shrink: 0; }
|
|
998
1011
|
[data-edrv-view] .edrv-floating-title > button { border: 0; background: transparent; color: inherit; cursor: pointer; }
|
|
999
1012
|
[data-edrv-view] .edrv-floating-grip { position: absolute; width: 16px; height: 16px; bottom: 0; right: 0; cursor: nwse-resize; touch-action: none; border-right: 3px solid var(--dsw-alias-border-l2, #cdd6da); border-bottom: 3px solid var(--dsw-alias-border-l2, #cdd6da); }
|
|
1000
1013
|
.edrv-drag-overlay { position: fixed; z-index: 150; pointer-events: none; box-sizing: border-box; border: 0; background: color-mix(in srgb, var(--dsw-alias-state-info-primary, #4d6bfe) 25%, transparent); }
|
|
@@ -11,6 +11,7 @@ import React from 'react'
|
|
|
11
11
|
import { createPortal } from 'react-dom'
|
|
12
12
|
import { EditorPane, EditorSplit, PassiveEditor } from './EditorPane.js'
|
|
13
13
|
import { FloatingEditor } from './FloatingEditor.js'
|
|
14
|
+
import { FloatingWindow } from './FloatingWindow.js'
|
|
14
15
|
import { createLayout, parseLayout, syncTabs, ownerOf, focusGroup, moveTab, returnGroup, GROUP_LIMIT, MAIN_GROUP } from '../state/editorLayout.js'
|
|
15
16
|
import { createTabDrag, hitTarget } from '../tabDrag.js'
|
|
16
17
|
import { createDocStore } from '../state/documentStore.js'
|
|
@@ -3505,8 +3506,7 @@ export function EditorView(props) {
|
|
|
3505
3506
|
//(focusDiff=true 跳过:差异审查对象必为本插件编辑的文本文件,审查优先);
|
|
3506
3507
|
// handler 缺失/失败(旧版无 openResource / seat 未挂)→ 回退现状页签打开,不阻断
|
|
3507
3508
|
if (!focusDiff && inNativePath(path) && tryNativeOpen(path)) return
|
|
3508
|
-
//
|
|
3509
|
-
setSvnDiff(null)
|
|
3509
|
+
// 对比浮窗独立保留,允许同时打开并编辑其他文件。
|
|
3510
3510
|
recordNav()
|
|
3511
3511
|
// G9:统一归一为页签规范形态(差异栏/启动器/树/命令栏等入口一致)
|
|
3512
3512
|
const normalized = addTabNorm(path, true)
|
|
@@ -3555,7 +3555,6 @@ export function EditorView(props) {
|
|
|
3555
3555
|
if (!document) return
|
|
3556
3556
|
flushSave()
|
|
3557
3557
|
saveViewState(active)
|
|
3558
|
-
setSvnDiff(null)
|
|
3559
3558
|
setDiskActive(null)
|
|
3560
3559
|
setPlanTabs((prev) => [...prev.filter((item) => item.address !== document.address), document].slice(-32))
|
|
3561
3560
|
setPlanActive(document.address)
|
|
@@ -3578,7 +3577,6 @@ export function EditorView(props) {
|
|
|
3578
3577
|
flushSave()
|
|
3579
3578
|
saveViewState(active)
|
|
3580
3579
|
recordNav()
|
|
3581
|
-
setSvnDiff(null)
|
|
3582
3580
|
const positioned = request.line != null || request.column != null
|
|
3583
3581
|
if (positioned && mdPreviewRef.current.has(path)) toggleMdPreview(path)
|
|
3584
3582
|
setError(null)
|
|
@@ -3611,7 +3609,7 @@ export function EditorView(props) {
|
|
|
3611
3609
|
const image = imgElRef.current
|
|
3612
3610
|
return {
|
|
3613
3611
|
sessionId, cwd, active, sequence: loadSeqRef.current, load: receiptLoad,
|
|
3614
|
-
mounted: Boolean(viewRootRef.current), blocked:
|
|
3612
|
+
mounted: Boolean(viewRootRef.current), blocked: false,
|
|
3615
3613
|
kind: isImageActive ? 'image' : isPdfActive ? 'pdf' : mdPreviewing ? 'markdown' : 'text',
|
|
3616
3614
|
contentPath, contentReady, modelMatches, positionMatches,
|
|
3617
3615
|
imageLoaded: Boolean(imageSrc && !imgBroken && image?.getAttribute('src') === imageSrc && image.complete && image.naturalWidth > 0),
|
|
@@ -3624,7 +3622,7 @@ export function EditorView(props) {
|
|
|
3624
3622
|
function receiptReady(request) {
|
|
3625
3623
|
if (request.plan) return request.sessionId === sessionId && planActive === request.plan.address
|
|
3626
3624
|
&& planTabs.some((item) => item.address === request.plan.address && item.markdown === request.plan.markdown)
|
|
3627
|
-
&& planHostRef.current?.getAttribute('data-plan-address') === request.plan.address
|
|
3625
|
+
&& planHostRef.current?.getAttribute('data-plan-address') === request.plan.address
|
|
3628
3626
|
return receiptReadyFor(request, receiptView(request))
|
|
3629
3627
|
}
|
|
3630
3628
|
/** @private @author ddj 2026年09月28号 @param request Claimed target. @returns Only its current load/render error. */
|
|
@@ -4013,27 +4011,6 @@ export function EditorView(props) {
|
|
|
4013
4011
|
: React.createElement('pre', { className: 'edrv-mdview-pre' }, text)
|
|
4014
4012
|
body = React.createElement('div', { className: 'edrv-mdview', ref: planHostRef, 'data-plan-address': activePlan.address },
|
|
4015
4013
|
React.createElement('div', { className: 'edrv-mdview-stage' }, React.createElement('div', { className: 'edrv-mdview-doc' }, render)))
|
|
4016
|
-
} else if (svnDiff) {
|
|
4017
|
-
// 差异视图优先占位(覆盖编辑器主体):只读审阅态,关闭后回到原编辑器
|
|
4018
|
-
body = React.createElement(SvnDiffPanel, {
|
|
4019
|
-
key: 'edrv-svndiff:' + svnDiff.scheme + ':' + svnDiff.path,
|
|
4020
|
-
monaco,
|
|
4021
|
-
path: svnDiff.path,
|
|
4022
|
-
base: svnDiff.base,
|
|
4023
|
-
working: svnDiff.working,
|
|
4024
|
-
reason: svnDiff.reason,
|
|
4025
|
-
message: svnDiff.message,
|
|
4026
|
-
leftLabel: svnDiff.leftLabel,
|
|
4027
|
-
rightLabel: svnDiff.rightLabel,
|
|
4028
|
-
onBack: svnDiff.logTarget !== undefined ? backToSvnLog : undefined,
|
|
4029
|
-
onClose: () => {
|
|
4030
|
-
setSvnDiff(null)
|
|
4031
|
-
if (svnUpdateReturn?.from === 'diff') {
|
|
4032
|
-
setSvnUpdateDialog(svnUpdateReturn.dialog)
|
|
4033
|
-
setSvnUpdateReturn(null)
|
|
4034
|
-
}
|
|
4035
|
-
},
|
|
4036
|
-
})
|
|
4037
4014
|
} else if (!monaco && !monacoErr) {
|
|
4038
4015
|
body = loadingBody(loadStage.message, loadStage.progress)
|
|
4039
4016
|
} else if (monacoErr) {
|
|
@@ -4329,7 +4306,7 @@ export function EditorView(props) {
|
|
|
4329
4306
|
}
|
|
4330
4307
|
|
|
4331
4308
|
// 「添加配置」按钮(VSCode 同款形态,编辑区右下角):仅插件专属 launch.json 打开且非差异审阅态显示
|
|
4332
|
-
const addCfgBtn = (active &&
|
|
4309
|
+
const addCfgBtn = (active && LAUNCH_JSON_RE.test(String(active).replace(/\\/g, '/')))
|
|
4333
4310
|
? React.createElement('button', {
|
|
4334
4311
|
className: 'edrv-addcfg',
|
|
4335
4312
|
title: '添加调试配置(插入到 configurations 数组开头)',
|
|
@@ -4367,12 +4344,12 @@ export function EditorView(props) {
|
|
|
4367
4344
|
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '复制安装命令', onClick: copyInstallCmd }, '复制命令'),
|
|
4368
4345
|
React.createElement('button', { className: 'edrv-side-hint-close', title: '关闭提示', 'aria-label': '关闭提示', onClick: dismissHint }, '×'))
|
|
4369
4346
|
: null
|
|
4370
|
-
const fullView = createPortal(React.createElement(
|
|
4347
|
+
const fullView = createPortal(React.createElement(React.Fragment, null, pathBar, sideHintEl, diskBanner(), editorArea, statusBar), fullHost, 'edrv-full-editor')
|
|
4371
4348
|
const mainCol = React.createElement('div', { className: 'edrv-main-col edrv-workbench', style: { flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' } },
|
|
4372
4349
|
React.createElement('div', { className: 'edrv-workbench-tools' }, tabRow),
|
|
4373
4350
|
renderTree(workbench.tree), fullView,
|
|
4374
4351
|
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))),
|
|
4375
|
-
dragPreview ? createPortal(React.createElement(
|
|
4352
|
+
dragPreview ? createPortal(React.createElement(React.Fragment, null,
|
|
4376
4353
|
dragPreview.rect ? React.createElement('div', { className: 'edrv-drag-overlay', style: dragPreview.rect }, React.createElement('span', { className: 'edrv-drop-label' }, dragPreview.label)) : null,
|
|
4377
4354
|
React.createElement('div', { className: 'edrv-drag-ghost', style: { left: dragPreview.point.x + 12, top: dragPreview.point.y + 12 } }, baseNameOf(dragPreview.path) + ' · ' + dragPreview.label)), document.body) : null)
|
|
4378
4355
|
// 编辑器根节点按 composer 顶部边界动态限高,底部对话区域继续由 DSH 原生渲染。
|
|
@@ -4517,6 +4494,23 @@ export function EditorView(props) {
|
|
|
4517
4494
|
},
|
|
4518
4495
|
})
|
|
4519
4496
|
: null
|
|
4497
|
+
/** @private @author ddj 2026年10月14号 关闭对比浮窗,按原 job 恢复来源更新窗口。 */
|
|
4498
|
+
function closeSvnDiff() {
|
|
4499
|
+
setSvnDiff(null)
|
|
4500
|
+
if (svnUpdateReturn?.from === 'diff') {
|
|
4501
|
+
setSvnUpdateDialog(svnUpdateReturn.dialog)
|
|
4502
|
+
setSvnUpdateReturn(null)
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
const svnDiffEl = svnDiff ? React.createElement(FloatingWindow, {
|
|
4506
|
+
key: 'edrv-svndiff-window:' + scope,
|
|
4507
|
+
title: '文件对比 · ' + baseNameOf(svnDiff.path), width: 1100, height: 680,
|
|
4508
|
+
owner: String(scope), onClose: closeSvnDiff,
|
|
4509
|
+
}, React.createElement(SvnDiffPanel, {
|
|
4510
|
+
...svnDiff, key: 'edrv-svndiff:' + svnDiff.scheme + ':' + svnDiff.path, monaco,
|
|
4511
|
+
onBack: svnDiff.logTarget !== undefined ? backToSvnLog : undefined,
|
|
4512
|
+
onClose: closeSvnDiff,
|
|
4513
|
+
})) : null
|
|
4520
4514
|
// W2-1 补丁对话框 / W2-2 目录对比对话框:portal 到 body,与日志弹窗同一挂载方式
|
|
4521
4515
|
const patchDialogEl = patchDialog
|
|
4522
4516
|
? React.createElement(SvnPatchDialog, {
|
|
@@ -4613,6 +4607,7 @@ export function EditorView(props) {
|
|
|
4613
4607
|
paletteEl,
|
|
4614
4608
|
svnLogEl,
|
|
4615
4609
|
svnUpdateEl,
|
|
4610
|
+
svnDiffEl,
|
|
4616
4611
|
patchDialogEl,
|
|
4617
4612
|
sumDialogEl,
|
|
4618
4613
|
dlogEl,
|
|
@@ -4625,6 +4620,7 @@ export function EditorView(props) {
|
|
|
4625
4620
|
paletteEl,
|
|
4626
4621
|
svnLogEl,
|
|
4627
4622
|
svnUpdateEl,
|
|
4623
|
+
svnDiffEl,
|
|
4628
4624
|
patchDialogEl,
|
|
4629
4625
|
sumDialogEl,
|
|
4630
4626
|
dlogEl,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/** In-page window
|
|
1
|
+
/** In-page editor window; closing returns tabs, never closes documents. @author ddj 2026年10月14号 */
|
|
2
2
|
import React from 'react'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import type { FloatRect } from '../state/editorLayout.js'
|
|
4
|
+
import { FloatingWindow } from './FloatingWindow.js'
|
|
5
5
|
|
|
6
6
|
export interface FloatingProps {
|
|
7
7
|
group: string
|
|
@@ -15,73 +15,19 @@ export interface FloatingProps {
|
|
|
15
15
|
children?: React.ReactNode
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
/** @public @author ddj 2026年10月14号 @param props Window geometry and group callbacks. @returns
|
|
19
|
-
export function FloatingEditor(props: FloatingProps): React.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const cleanup = React.useRef<(() => void) | null>(null)
|
|
23
|
-
/** @private @author ddj 2026年10月14号 Cancel a gesture on unmount without altering tabs. */
|
|
24
|
-
function cancel(): void { cleanup.current?.(); cleanup.current = null }
|
|
25
|
-
React.useEffect(() => cancel, [])
|
|
26
|
-
React.useEffect(() => {
|
|
27
|
-
/** @private @author ddj 2026年10月14号 Keep title/chrome visible after viewport changes. */
|
|
28
|
-
function resize(): void {
|
|
29
|
-
const current = latest.current
|
|
30
|
-
current.onBounds(current.group, clampFloat(current.rect, window.innerWidth, window.innerHeight))
|
|
31
|
-
}
|
|
32
|
-
window.addEventListener('resize', resize)
|
|
33
|
-
resize()
|
|
34
|
-
return () => window.removeEventListener('resize', resize)
|
|
35
|
-
}, [])
|
|
36
|
-
|
|
37
|
-
/** @private @author ddj 2026年10月14号 @param event Handle press. @param sizing Resize rather than move. */
|
|
38
|
-
function start(event: React.PointerEvent, sizing: boolean): void {
|
|
39
|
-
if (event.button !== 0 || !event.isPrimary) return
|
|
40
|
-
cancel()
|
|
41
|
-
event.preventDefault()
|
|
42
|
-
const initial = latest.current.rect
|
|
43
|
-
const x = event.clientX, y = event.clientY, pointer = event.pointerId
|
|
44
|
-
let frame: number | null = null
|
|
45
|
-
let next = initial
|
|
46
|
-
latest.current.onFocus(props.group)
|
|
47
|
-
/** @private @author ddj 2026年10月14号 Apply at most one geometry change per animation frame. */
|
|
48
|
-
function paint(): void { frame = null; latest.current.onBounds(props.group, next) }
|
|
49
|
-
/** @private @author ddj 2026年10月14号 @param moveEvent Pointer movement for the originating pointer. */
|
|
50
|
-
function move(moveEvent: PointerEvent): void {
|
|
51
|
-
if (moveEvent.pointerId !== pointer) return
|
|
52
|
-
moveEvent.preventDefault()
|
|
53
|
-
const dx = moveEvent.clientX - x, dy = moveEvent.clientY - y
|
|
54
|
-
next = clampFloat(sizing ? { ...initial, width: initial.width + dx, height: initial.height + dy } : { ...initial, left: initial.left + dx, top: initial.top + dy }, window.innerWidth, window.innerHeight)
|
|
55
|
-
if (frame === null) frame = requestAnimationFrame(paint)
|
|
56
|
-
}
|
|
57
|
-
/** @private @author ddj 2026年10月14号 Remove gesture listeners and pending frames. */
|
|
58
|
-
function stop(): void {
|
|
59
|
-
if (frame !== null) { cancelAnimationFrame(frame); paint() }
|
|
60
|
-
window.removeEventListener('pointermove', move, true)
|
|
61
|
-
window.removeEventListener('pointerup', stop, true)
|
|
62
|
-
window.removeEventListener('pointercancel', stop, true)
|
|
63
|
-
window.removeEventListener('blur', stop)
|
|
64
|
-
cleanup.current = null
|
|
65
|
-
}
|
|
66
|
-
cleanup.current = stop
|
|
67
|
-
window.addEventListener('pointermove', move, true)
|
|
68
|
-
window.addEventListener('pointerup', stop, true)
|
|
69
|
-
window.addEventListener('pointercancel', stop, true)
|
|
70
|
-
window.addEventListener('blur', stop)
|
|
71
|
-
}
|
|
72
|
-
/** @private @author ddj 2026年10月14号 @param event Title press. Move window without stealing button presses. */
|
|
73
|
-
function moveStart(event: React.PointerEvent): void { if (!(event.target as Element).closest('button')) start(event, false) }
|
|
74
|
-
/** @private @author ddj 2026年10月14号 @param event Resize grip press. */
|
|
75
|
-
function sizeStart(event: React.PointerEvent): void { start(event, true) }
|
|
18
|
+
/** @public @author ddj 2026年10月14号 @param props Window geometry and group callbacks. @returns Shared window chrome preserving tab ownership. */
|
|
19
|
+
export function FloatingEditor(props: FloatingProps): React.ReactElement {
|
|
20
|
+
/** @private @author ddj 2026年10月14号 @param rect Updated window bounds. */
|
|
21
|
+
function bounds(rect: FloatRect): void { props.onBounds(props.group, rect) }
|
|
76
22
|
/** @private @author ddj 2026年10月14号 Return all tabs to the main group. */
|
|
77
|
-
function returnTabs(): void {
|
|
23
|
+
function returnTabs(): void { props.onReturn(props.group) }
|
|
78
24
|
/** @private @author ddj 2026年10月14号 Bring the group to the foreground. */
|
|
79
|
-
function focus(): void {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
React.createElement('
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
25
|
+
function focus(): void { props.onFocus(props.group) }
|
|
26
|
+
const actions = React.createElement(React.Fragment, null,
|
|
27
|
+
React.createElement('button', { type: 'button', title: '将全部文件移回主编辑区', onClick: returnTabs }, '移回主编辑区'),
|
|
28
|
+
React.createElement('button', { type: 'button', title: '关闭浮窗并将文件移回主编辑区', 'aria-label': '关闭浮窗并移回文件', onClick: returnTabs }, '×'))
|
|
29
|
+
return React.createElement(FloatingWindow, {
|
|
30
|
+
title: '浮动编辑组', owner: props.owner, group: props.group, rect: props.rect,
|
|
31
|
+
closeOnEsc: false, onBounds: bounds, onFocus: focus, actions,
|
|
32
|
+
}, props.children)
|
|
87
33
|
}
|