dsh-vscode-mode 0.14.13 → 0.15.2
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 +7 -3
- package/lib/client.js +2967 -1329
- package/lib/client.js.map +1 -1
- package/lib/index.js +217 -5
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/paths.ts +2 -0
- package/src/client/state/documentStore.ts +148 -0
- package/src/client/state/editorLayout.ts +179 -0
- package/src/client/state/modelCache.ts +16 -7
- package/src/client/styles/editor.css +33 -3
- package/src/client/svnNotice.ts +1 -1
- package/src/client/tabDrag.ts +137 -0
- package/src/client/tabMenu.ts +15 -3
- package/src/client/ui/EditorPane.ts +130 -0
- package/src/client/ui/EditorView.ts +358 -97
- package/src/client/ui/FloatingEditor.ts +87 -0
- package/src/client/ui/SvnUpdateDialog.ts +143 -29
- package/src/client/ui/svnUpdateModel.ts +45 -1
- package/src/shared/rpc.ts +5 -1
- package/src/shared/svn.ts +10 -0
- package/src/svn.ts +70 -5
- package/src/svnResolve.ts +33 -0
package/src/client/tabMenu.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* EditorView 只把结果映射成 ContextMenu 的 entries 并派发动作。
|
|
5
5
|
*
|
|
6
6
|
* 与参考图(VS Code / CodeBuddy 页签右键菜单)的对齐口径:
|
|
7
|
-
* 保留其分组顺序(对话 / 关闭族 / 路径 / 定位 /
|
|
8
|
-
*
|
|
7
|
+
* 保留其分组顺序(对话 / 关闭族 / 分组 / 路径 / 定位 / 固定)。
|
|
8
|
+
* 分屏与页面内浮窗由 workbench 能力显式控制,不伪造系统窗口或复制编辑器能力。
|
|
9
9
|
* 只显示**真实已绑定**的键位;参考图里的两步弦(Ctrl+K W 等)引擎不支持,故不伪造。
|
|
10
10
|
*
|
|
11
11
|
* 「固定 = 保护」:关闭其他 / 关闭右侧 / 关闭已保存 / 全部关闭 一律不关固定页签,
|
|
@@ -44,6 +44,10 @@ export interface TabMenuState {
|
|
|
44
44
|
svnStatusOfTarget?: SvnItemStatus
|
|
45
45
|
/** 「关闭」项的键位提示(缺省无提示;由调用方读 chordOf 注入)。 */
|
|
46
46
|
closeChord?: string | null
|
|
47
|
+
/** Real workbench capabilities; omitted for legacy consumers. */
|
|
48
|
+
canSplit?: boolean
|
|
49
|
+
canFloat?: boolean
|
|
50
|
+
canReturn?: boolean
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
/** 一条页签菜单项(ContextMenu 的展示形状 + 动作 id)。 */
|
|
@@ -62,7 +66,7 @@ export interface TabMenuEntry {
|
|
|
62
66
|
export const CLOSE_MENU_IDS = ['close', 'close-others', 'close-right', 'close-saved', 'close-all'] as const
|
|
63
67
|
|
|
64
68
|
/** 本架构不支持的条目 id(参考图有、浏览器单编辑器实例无法实现)——显式登记以防误加。 */
|
|
65
|
-
export const UNSUPPORTED_MENU_IDS = ['split-
|
|
69
|
+
export const UNSUPPORTED_MENU_IDS = ['split-move', 'copy-new-window'] as const
|
|
66
70
|
|
|
67
71
|
/**
|
|
68
72
|
* 构建 SVN 组条目(动作清单来自 shared/svnActions.ts,与树菜单/命令栏同一份规则)。
|
|
@@ -123,6 +127,14 @@ export function buildTabMenu(state: TabMenuState): TabMenuEntry[] {
|
|
|
123
127
|
{ id: 'close-right', label: '关闭右侧标签页', disabled: nothingToClose(closeRight(tabs, path, active), tabs) },
|
|
124
128
|
{ id: 'close-saved', label: '关闭已保存', disabled: nothingToClose(closeSaved(tabs, dirty, active), tabs) },
|
|
125
129
|
{ id: 'close-all', label: '全部关闭', disabled: nothingToClose(closeAll(tabs, active), tabs) },
|
|
130
|
+
...(state.canSplit !== undefined ? [
|
|
131
|
+
{ id: 'split-left', label: '向左分屏', separator: true, disabled: !state.canSplit },
|
|
132
|
+
{ id: 'split-right', label: '向右分屏', disabled: !state.canSplit },
|
|
133
|
+
{ id: 'split-top', label: '向上分屏', disabled: !state.canSplit },
|
|
134
|
+
{ id: 'split-bottom', label: '向下分屏', disabled: !state.canSplit },
|
|
135
|
+
{ id: 'move-new-window', label: '移到页面内浮窗', disabled: !state.canFloat },
|
|
136
|
+
{ id: 'return-main', label: '移回主编辑区', disabled: !state.canReturn },
|
|
137
|
+
] : []),
|
|
126
138
|
{ id: 'copy-path', label: '复制路径', separator: true },
|
|
127
139
|
{ id: 'copy-relative-path', label: '复制相对路径', disabled: !hasCwd(cwd) },
|
|
128
140
|
{ id: 'reveal-in-os', label: '在文件资源管理器中显示', separator: true, disabled: !hasSession },
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/** Independent group views; workbench owns documents, saves and commands. @author ddj 2026年10月14号 */
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { createViewKeeper, type ViewEditor } from '../state/editorViewState.js'
|
|
4
|
+
import type { Group } from '../state/editorLayout.js'
|
|
5
|
+
import type { TabLike } from '../tabActions.js'
|
|
6
|
+
|
|
7
|
+
export interface PaneProps {
|
|
8
|
+
group: Group
|
|
9
|
+
focused: boolean
|
|
10
|
+
dirty: Record<string, boolean>
|
|
11
|
+
onSelect(group: string, path?: string): void
|
|
12
|
+
onClose(path: string): void
|
|
13
|
+
onMenu(path: string, event: React.MouseEvent): void
|
|
14
|
+
onDrag(path: string, event: React.PointerEvent): void
|
|
15
|
+
consumeClick(): boolean
|
|
16
|
+
children?: React.ReactNode
|
|
17
|
+
extras?: React.ReactNode
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @public @author ddj 2026年10月14号 @param props Group callbacks and file view. @returns Accessible tab strip and drop target. */
|
|
21
|
+
export function EditorPane(props: PaneProps): React.ReactElement {
|
|
22
|
+
const { group } = props
|
|
23
|
+
/** @private @author ddj 2026年10月14号 @param tab File record. @returns Draggable tab with isolated close control. */
|
|
24
|
+
function renderTab(tab: TabLike): React.ReactElement {
|
|
25
|
+
/** @private @author ddj 2026年10月14号 @param event Primary press. Start trusted file drag. */
|
|
26
|
+
function press(event: React.PointerEvent): void { props.onDrag(tab.path, event) }
|
|
27
|
+
/** @private @author ddj 2026年10月14号 Select unless release completed a drag. */
|
|
28
|
+
function select(): void { if (!props.consumeClick()) props.onSelect(group.id, tab.path) }
|
|
29
|
+
/** @private @author ddj 2026年10月14号 @param event Context event. Open file actions. */
|
|
30
|
+
function menu(event: React.MouseEvent): void { event.preventDefault(); event.stopPropagation(); props.onMenu(tab.path, event) }
|
|
31
|
+
/** @private @author ddj 2026年10月14号 @param event Close activation. Never select tab. */
|
|
32
|
+
function close(event: React.MouseEvent): void { event.stopPropagation(); props.onClose(tab.path) }
|
|
33
|
+
/** @private @author ddj 2026年10月14号 @param event Close press. Never start a drag. */
|
|
34
|
+
function stop(event: React.PointerEvent): void { event.stopPropagation() }
|
|
35
|
+
/** @private @author ddj 2026年10月14号 @param event Keyboard activation. */
|
|
36
|
+
function key(event: React.KeyboardEvent): void { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); props.onSelect(group.id, tab.path) } }
|
|
37
|
+
return React.createElement('div', {
|
|
38
|
+
key: tab.path, 'data-edrv-tab-path': tab.path, role: 'tab', tabIndex: 0, 'aria-selected': tab.path === group.active,
|
|
39
|
+
className: 'edrv-tab' + (tab.path === group.active ? ' edrv-tab-active' : '') + (tab.pinned ? ' edrv-tab-pinned' : ''),
|
|
40
|
+
title: tab.path + (tab.pinned ? '(已固定)' : ''), onPointerDown: press, onClick: select, onContextMenu: menu, onKeyDown: key,
|
|
41
|
+
}, tab.pinned ? React.createElement('span', { className: 'edrv-tab-pin' }, '📌') : null,
|
|
42
|
+
React.createElement('span', { className: 'edrv-group-tab-name' }, tab.path.split(/[\\/]/).pop()),
|
|
43
|
+
props.dirty[tab.path] ? React.createElement('span', { className: 'edrv-tab-star', title: '未保存修改' }, '*') : null,
|
|
44
|
+
tab.pinned ? null : React.createElement('button', { className: 'edrv-tab-x', type: 'button', 'aria-label': '关闭 ' + tab.path, title: '关闭', onPointerDown: stop, onClick: close }, '×'))
|
|
45
|
+
}
|
|
46
|
+
/** @private @author ddj 2026年10月14号 @param event Group background press. Allow an empty main group to receive new files. */
|
|
47
|
+
function focusEmpty(event: React.PointerEvent): void {
|
|
48
|
+
if (!group.active && !props.focused && event.button === 0) props.onSelect(group.id)
|
|
49
|
+
}
|
|
50
|
+
return React.createElement('section', { className: 'edrv-group' + (props.focused ? ' edrv-group-focused' : ''), 'data-edrv-group': group.id, onPointerDown: focusEmpty },
|
|
51
|
+
React.createElement('div', { className: 'edrv-tabs', role: 'tablist', 'data-edrv-group-tabs': group.id }, group.tabs.map(renderTab), props.extras),
|
|
52
|
+
React.createElement('div', { className: 'edrv-group-body', role: 'tabpanel' }, props.children))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @public @author ddj 2026年10月14号 @param editor View instance. @param scope Workspace. @param path Canonical path. @param model Shared content. @returns Keeper whose disposal never disposes the model. */
|
|
56
|
+
export function bindPaneView(editor: ViewEditor, scope: string, path: string, model: unknown) {
|
|
57
|
+
const keeper = createViewKeeper(editor)
|
|
58
|
+
keeper.bind(scope, path, model)
|
|
59
|
+
return keeper
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PassiveProps {
|
|
63
|
+
path: string | null
|
|
64
|
+
mount(host: HTMLDivElement, path: string): (() => void) | undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @public @author ddj 2026年10月14号 @param props Workbench-owned mounting contract. @returns Independent visible file view without global command listeners. */
|
|
68
|
+
export function PassiveEditor(props: PassiveProps): React.ReactElement {
|
|
69
|
+
const host = React.useRef<HTMLDivElement>(null)
|
|
70
|
+
React.useLayoutEffect(() => {
|
|
71
|
+
if (host.current && props.path) return props.mount(host.current, props.path)
|
|
72
|
+
}, [props.path, props.mount])
|
|
73
|
+
return React.createElement('div', { ref: host, className: 'edrv-passive-view' })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SplitProps {
|
|
77
|
+
axis: 'horizontal' | 'vertical'
|
|
78
|
+
ratio: number
|
|
79
|
+
onRatio(ratio: number): void
|
|
80
|
+
first: React.ReactNode
|
|
81
|
+
second: React.ReactNode
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @public @author ddj 2026年10月14号 @param props Split geometry. @returns Resizable split with keyboard equivalents. */
|
|
85
|
+
export function EditorSplit(props: SplitProps): React.ReactElement {
|
|
86
|
+
const host = React.useRef<HTMLDivElement>(null)
|
|
87
|
+
const clean = React.useRef<(() => void) | null>(null)
|
|
88
|
+
const latest = React.useRef(props)
|
|
89
|
+
latest.current = props
|
|
90
|
+
React.useEffect(() => () => clean.current?.(), [])
|
|
91
|
+
/** @private @author ddj 2026年10月14号 @param event Separator press. Resize only this branch. */
|
|
92
|
+
function start(event: React.PointerEvent): void {
|
|
93
|
+
if (event.button !== 0 || !host.current) return
|
|
94
|
+
clean.current?.()
|
|
95
|
+
event.preventDefault()
|
|
96
|
+
const rect = host.current.getBoundingClientRect(), pointer = event.pointerId
|
|
97
|
+
/** @private @author ddj 2026年10月14号 @param value Pointer movement. Keep both leaves usable. */
|
|
98
|
+
function move(value: PointerEvent): void {
|
|
99
|
+
if (value.pointerId !== pointer) return
|
|
100
|
+
const horizontal = latest.current.axis === 'horizontal'
|
|
101
|
+
const size = horizontal ? rect.width : rect.height
|
|
102
|
+
const min = Math.min(0.45, (horizontal ? 240 : 160) / size)
|
|
103
|
+
const at = horizontal ? value.clientX - rect.left : value.clientY - rect.top
|
|
104
|
+
latest.current.onRatio(Math.max(min, Math.min(1 - min, at / size)))
|
|
105
|
+
}
|
|
106
|
+
/** @private @author ddj 2026年10月14号 Remove resize listeners. */
|
|
107
|
+
function stop(): void {
|
|
108
|
+
window.removeEventListener('pointermove', move)
|
|
109
|
+
window.removeEventListener('pointerup', stop)
|
|
110
|
+
window.removeEventListener('pointercancel', stop)
|
|
111
|
+
window.removeEventListener('blur', stop)
|
|
112
|
+
clean.current = null
|
|
113
|
+
}
|
|
114
|
+
clean.current = stop
|
|
115
|
+
window.addEventListener('pointermove', move)
|
|
116
|
+
window.addEventListener('pointerup', stop)
|
|
117
|
+
window.addEventListener('pointercancel', stop)
|
|
118
|
+
window.addEventListener('blur', stop)
|
|
119
|
+
}
|
|
120
|
+
/** @private @author ddj 2026年10月14号 @param event Keyboard resizing. Arrow keys change ratio. */
|
|
121
|
+
function key(event: React.KeyboardEvent): void {
|
|
122
|
+
if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) return
|
|
123
|
+
event.preventDefault()
|
|
124
|
+
props.onRatio(Math.max(0.1, Math.min(0.9, props.ratio + (event.key === 'ArrowLeft' || event.key === 'ArrowUp' ? -0.05 : 0.05))))
|
|
125
|
+
}
|
|
126
|
+
return React.createElement('div', { ref: host, className: 'edrv-split edrv-split-' + props.axis },
|
|
127
|
+
React.createElement('div', { className: 'edrv-split-child', style: { flex: props.ratio } }, props.first),
|
|
128
|
+
React.createElement('div', { className: 'edrv-split-handle', role: 'separator', tabIndex: 0, 'aria-orientation': props.axis === 'horizontal' ? 'vertical' : 'horizontal', 'aria-valuenow': Math.round(props.ratio * 100), 'aria-valuemin': 10, 'aria-valuemax': 90, onPointerDown: start, onKeyDown: key }),
|
|
129
|
+
React.createElement('div', { className: 'edrv-split-child', style: { flex: 1 - props.ratio } }, props.second))
|
|
130
|
+
}
|