dsh-vscode-mode 0.14.8 → 0.14.10
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 +11 -7
- package/lib/client.js +246 -4
- package/lib/client.js.map +1 -1
- package/lib/index.js +3 -6
- package/lib/index.js.map +1 -1
- package/package.json +12 -7
- package/src/client/editorModelState.ts +16 -0
- package/src/client/index.ts +9 -1
- package/src/client/monaco/editActions.ts +58 -0
- package/src/client/pageReload.ts +73 -0
- package/src/client/shortcutsOfficial.ts +14 -3
- package/src/client/state/uiStatePersist.ts +22 -0
- package/src/client/ui/EditorView.ts +25 -1
- package/src/client/ui/commandCatalog.ts +16 -0
- package/src/compat.ts +0 -9
- package/src/shared/keybindings.ts +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.10",
|
|
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": [
|
|
@@ -48,19 +48,24 @@
|
|
|
48
48
|
"@vscode/ripgrep": "^1.18.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@deepseek-ai/dsh-client-ui-slots": "
|
|
52
|
-
"@deepseek-ai/dsh-settings": "
|
|
53
|
-
"@deepseek-ai/dsh-client-ui-settings": "
|
|
54
|
-
"@deepseek-ai/dsh-client-ui-primitives": "
|
|
55
|
-
"@deepseek-ai/dsh-llm": "
|
|
56
|
-
"@deepseek-ai/dsh-tools": "
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-slots": "*",
|
|
52
|
+
"@deepseek-ai/dsh-settings": "*",
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-settings": "*",
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-primitives": "*",
|
|
55
|
+
"@deepseek-ai/dsh-llm": "*",
|
|
56
|
+
"@deepseek-ai/dsh-tools": "*",
|
|
57
57
|
"cordis": ">=4.0.0-rc <5",
|
|
58
58
|
"react": "^18.2.0",
|
|
59
59
|
"react-dom": "^18.2.0",
|
|
60
60
|
"schemastery": "^3.18.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.6-alpha.1",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.0-rc.8",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.0.1-rc.1",
|
|
66
|
+
"@deepseek-ai/dsh-llm": "0.0.1-rc.1",
|
|
63
67
|
"@deepseek-ai/dsh-settings": "0.1.0-rc.8",
|
|
68
|
+
"@deepseek-ai/dsh-tools": "0.0.1-rc.1",
|
|
64
69
|
"@types/node": "^24.13.3",
|
|
65
70
|
"@types/react": "^18.3.1",
|
|
66
71
|
"@types/react-dom": "^18.3.5",
|
|
@@ -19,6 +19,22 @@ export const EDITOR_ROOT_SELECTOR = '.edrv-editor-row'
|
|
|
19
19
|
/** Monaco 编辑器实例选择器(限定在插件编辑器行内,排除官方预览等其它 Monaco)。 */
|
|
20
20
|
export const EDITOR_MODEL_SELECTOR = EDITOR_ROOT_SELECTOR + ' .monaco-editor'
|
|
21
21
|
|
|
22
|
+
/** @public @author ddj 2026年10月12号 @param target Optional shortcut target. @returns Whether focus belongs to plugin code input, excluding widgets and read-only editors. */
|
|
23
|
+
export function hasCodeFocus(target?: unknown): boolean {
|
|
24
|
+
if (typeof document === 'undefined') return false
|
|
25
|
+
const node = target ?? document.activeElement
|
|
26
|
+
if (!(node instanceof Element)) return false
|
|
27
|
+
const editor = node.closest(EDITOR_MODEL_SELECTOR)
|
|
28
|
+
if (!editor || editor.classList.contains('readonly')) return false
|
|
29
|
+
if (node.closest('.find-widget, .suggest-widget, .rename-box, .monaco-inputbox')) return false
|
|
30
|
+
const host = typeof window === 'undefined' ? null : window as unknown as {
|
|
31
|
+
monaco?: { editor?: { getEditors?(): Array<{ getDomNode(): unknown; hasTextFocus(): boolean; getModel(): unknown }> } }
|
|
32
|
+
}
|
|
33
|
+
return host?.monaco?.editor?.getEditors?.().some((item) =>
|
|
34
|
+
item.getDomNode() === editor && item.hasTextFocus() && Boolean(item.getModel()),
|
|
35
|
+
) ?? false
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
/**
|
|
23
39
|
* 已打开文件页签选择器(页签栏在编辑器行内;空态时无此节点)。
|
|
24
40
|
* 判据同样不依赖 Monaco:图片/PDF 页签没有 Monaco 实例,但仍是「有页签可关」。
|
package/src/client/index.ts
CHANGED
|
@@ -65,7 +65,8 @@ import { disposeLaunchJson } from './dap/launchSnippetProvider.js'
|
|
|
65
65
|
import { disposeAiInline } from './ai/inlineProvider.js'
|
|
66
66
|
import { readSessionScope, subscribeScope } from './sessionScope.js'
|
|
67
67
|
import { installPlanBridge } from './planBridge.js'
|
|
68
|
-
import {
|
|
68
|
+
import { installPageReload } from './pageReload.js'
|
|
69
|
+
import { flushPageState, flushUiState, hydrateUiState, installUiStatePersist, onUiStateReady, reportUiStateEvent, uiStateSettled } from './state/uiStatePersist.js'
|
|
69
70
|
import { workspaceScopeOf } from './state/scopeStore.js'
|
|
70
71
|
import { dapStore } from './dap/store.js'
|
|
71
72
|
import type { CompatAdapter } from '../shared/compat.js'
|
|
@@ -185,6 +186,13 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
185
186
|
|
|
186
187
|
// 指令系统(命令栏 + 指令注册表)先装配:命令栏在被任何 React slot 渲染前即可唤起
|
|
187
188
|
const commands = setupCommands(ctx)
|
|
189
|
+
/** @private @author ddj 2026年10月12号 @param message Refresh refusal; surfaced in mounted editor status and diagnostic log. */
|
|
190
|
+
const notifyReload = (message: string): void => {
|
|
191
|
+
log.warn(message)
|
|
192
|
+
window.dispatchEvent(new CustomEvent('edrv:reload-status', { detail: message }))
|
|
193
|
+
window.alert(message)
|
|
194
|
+
}
|
|
195
|
+
ctx.effect(() => installPageReload(flushPageState, notifyReload), 'vscode-mode: guarded page reload')
|
|
188
196
|
|
|
189
197
|
const registry: FileOpenerRegistry = createFileOpenerRegistry()
|
|
190
198
|
const workspaces = ctx.get('workspaces')
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Plugin-local Monaco editing bindings. @author ddj 2026年10月12号 */
|
|
2
|
+
export interface EditTarget {
|
|
3
|
+
getModel(): unknown
|
|
4
|
+
getOption(option: number): unknown
|
|
5
|
+
hasTextFocus(): boolean
|
|
6
|
+
focus(): void
|
|
7
|
+
trigger(source: string, action: string, payload: null): void
|
|
8
|
+
createContextKey(name: string, value: boolean): { reset(): void }
|
|
9
|
+
onDidDispose(listener: () => void): { dispose(): void }
|
|
10
|
+
addAction(action: { id: string; label: string; precondition: string; contextMenuGroupId: string; run(): void }): { dispose(): void }
|
|
11
|
+
}
|
|
12
|
+
export interface EditMonaco {
|
|
13
|
+
KeyMod: { CtrlCmd: number }
|
|
14
|
+
KeyCode: { Slash: number; KeyD: number }
|
|
15
|
+
editor: {
|
|
16
|
+
EditorOption: { readOnly: number }
|
|
17
|
+
addKeybindingRules(rules: Array<{ keybinding: number; command: string; when: string }>): { dispose(): void }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @public @author ddj 2026年10月12号 @param editor Target editor. @param monaco Monaco API. @param action Native action. @returns Whether a writable model accepted the action. @sideEffects Restores focus and runs an undoable native edit. */
|
|
22
|
+
export function runEditAction(editor: EditTarget | null, monaco: EditMonaco | null, action: string): boolean {
|
|
23
|
+
if (!editor?.getModel() || !monaco || editor.getOption(monaco.editor.EditorOption.readOnly)) return false
|
|
24
|
+
editor.focus()
|
|
25
|
+
editor.trigger('edrv-edit', action, null)
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @public @author ddj 2026年10月12号 @param editor Plugin editor. @param monaco Monaco API. @returns Idempotent disposer. @sideEffects Overrides only plugin-local bindings and menu entries until editor disposal. */
|
|
30
|
+
export function bindEditActions(editor: EditTarget, monaco: EditMonaco): () => void {
|
|
31
|
+
const local = editor.createContextKey('edrvLocalEdit', true)
|
|
32
|
+
const focus = 'edrvLocalEdit && editorTextFocus'
|
|
33
|
+
const writable = focus + ' && !editorReadonly'
|
|
34
|
+
const rules = monaco.editor.addKeybindingRules([
|
|
35
|
+
{ keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.Slash, command: '', when: focus },
|
|
36
|
+
{ keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyD, command: 'editor.action.copyLinesDownAction', when: writable },
|
|
37
|
+
])
|
|
38
|
+
/** @private @author ddj 2026年10月12号 Runs the native language-aware comment action. */
|
|
39
|
+
const comment = (): void => { runEditAction(editor, monaco, 'editor.action.commentLine') }
|
|
40
|
+
/** @private @author ddj 2026年10月12号 Runs the native multi-selection line-copy action. */
|
|
41
|
+
const duplicate = (): void => { runEditAction(editor, monaco, 'editor.action.copyLinesDownAction') }
|
|
42
|
+
const actions = [
|
|
43
|
+
editor.addAction({ id: 'edrv.toggleComment', label: '切换行注释(Ctrl+Alt+/)', precondition: writable, contextMenuGroupId: '1_edrv', run: comment }),
|
|
44
|
+
editor.addAction({ id: 'edrv.duplicateLine', label: '向下复制行(Ctrl+D,编辑器本地)', precondition: writable, contextMenuGroupId: '1_edrv', run: duplicate }),
|
|
45
|
+
]
|
|
46
|
+
let disposed = false
|
|
47
|
+
/** @private @author ddj 2026年10月12号 Releases global rule contributions owned by this instance. */
|
|
48
|
+
const dispose = (): void => {
|
|
49
|
+
if (disposed) return
|
|
50
|
+
disposed = true
|
|
51
|
+
rules.dispose()
|
|
52
|
+
for (const action of actions) action.dispose()
|
|
53
|
+
local.reset()
|
|
54
|
+
lifetime.dispose()
|
|
55
|
+
}
|
|
56
|
+
const lifetime = editor.onDidDispose(dispose)
|
|
57
|
+
return dispose
|
|
58
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Guarded page-only reload; never restarts Host. @author ddj 2026年10月12号 */
|
|
2
|
+
export interface ReloadGuard { check(): string | null; prepare(): void }
|
|
3
|
+
export interface ReloadDeps {
|
|
4
|
+
flush(): Promise<void>
|
|
5
|
+
reload(): void
|
|
6
|
+
notify(message: string): void
|
|
7
|
+
timeoutMs?: number
|
|
8
|
+
}
|
|
9
|
+
const guards = new Set<ReloadGuard>()
|
|
10
|
+
|
|
11
|
+
/** @public @author ddj 2026年10月12号 @param guard Live editor checks and view-state capture. @returns Unregister function. */
|
|
12
|
+
export function addReloadGuard(guard: ReloadGuard): () => void {
|
|
13
|
+
guards.add(guard)
|
|
14
|
+
return () => { guards.delete(guard) }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** @private @author ddj 2026年10月12号 @param checks Current editor guards. @returns First unsafe state, or null. */
|
|
18
|
+
function reloadBlock(checks: Iterable<ReloadGuard>): string | null {
|
|
19
|
+
for (const guard of checks) {
|
|
20
|
+
const reason = guard.check()
|
|
21
|
+
if (reason) return reason
|
|
22
|
+
}
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @public @author ddj 2026年10月12号 @param deps Page operations. @param checks Live editor guards. @returns Serialized request and cancellation. @sideEffects Flushes view state then reloads only when still safe. */
|
|
27
|
+
export function createPageReload(deps: ReloadDeps, checks: Iterable<ReloadGuard> = guards) {
|
|
28
|
+
let busy = false
|
|
29
|
+
let disposed = false
|
|
30
|
+
/** @private @author ddj 2026年10月12号 @returns Completion of one guarded reload attempt. */
|
|
31
|
+
async function request(): Promise<void> {
|
|
32
|
+
if (busy || disposed) return
|
|
33
|
+
busy = true
|
|
34
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
35
|
+
try {
|
|
36
|
+
const reason = reloadBlock(checks)
|
|
37
|
+
if (reason) { deps.notify('未刷新:' + reason); return }
|
|
38
|
+
for (const guard of checks) guard.prepare()
|
|
39
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
40
|
+
timer = setTimeout(() => reject(new Error('界面状态保存超时')), deps.timeoutMs ?? 5000)
|
|
41
|
+
})
|
|
42
|
+
await Promise.race([deps.flush(), timeout])
|
|
43
|
+
if (disposed) return
|
|
44
|
+
const changed = reloadBlock(checks)
|
|
45
|
+
if (changed) { deps.notify('未刷新:' + changed); return }
|
|
46
|
+
deps.reload()
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (!disposed) deps.notify('未刷新:' + String(error))
|
|
49
|
+
} finally {
|
|
50
|
+
clearTimeout(timer)
|
|
51
|
+
busy = false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { request, dispose: () => { disposed = true } }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @public @author ddj 2026年10月12号 @returns Whether no live modal blocks page commands. */
|
|
58
|
+
export function pageKeysAllowed(): boolean {
|
|
59
|
+
if (typeof document === 'undefined') return true
|
|
60
|
+
return document.querySelector('[role="dialog"], [aria-modal="true"]') === null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @public @author ddj 2026年10月12号 @param flush Durable UI-state flush. @param notify User-visible failure sink. @returns Root-scope listener disposer. */
|
|
64
|
+
export function installPageReload(flush: () => Promise<void>, notify: (message: string) => void): () => void {
|
|
65
|
+
const controller = createPageReload({ flush, notify, reload: () => window.location.reload() })
|
|
66
|
+
/** @private @author ddj 2026年10月12号 Ignores modal-blocked commands even from programmatic callers. */
|
|
67
|
+
const onReload = (): void => { if (pageKeysAllowed()) void controller.request() }
|
|
68
|
+
window.addEventListener('edrv.command.reloadWindow', onReload)
|
|
69
|
+
return () => {
|
|
70
|
+
controller.dispose()
|
|
71
|
+
window.removeEventListener('edrv.command.reloadWindow', onReload)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -11,6 +11,15 @@ import { bindingToChord, chordToBinding, defaultKeybindings, normalizeKeybinding
|
|
|
11
11
|
import { applyKeyAria, matchEvent, parseChords } from './keybindings.js'
|
|
12
12
|
import type { OfficialBinding, ShortcutProfileKey } from '../shared/keybindings.js'
|
|
13
13
|
import { log } from './log.js'
|
|
14
|
+
import { hasCodeFocus } from './editorModelState.js'
|
|
15
|
+
import { pageKeysAllowed } from './pageReload.js'
|
|
16
|
+
|
|
17
|
+
/** @private @author ddj 2026年10月12号 @param id Command id. @param target Shortcut target. @returns Whether new page commands can safely run from this context. */
|
|
18
|
+
function shortcutAllowed(id: string, target?: unknown): boolean {
|
|
19
|
+
if (id === 'edrv.toggleComment' || id === 'edrv.duplicateLine') return pageKeysAllowed() && hasCodeFocus(target)
|
|
20
|
+
if (id === 'edrv.reloadWindow') return pageKeysAllowed()
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
14
23
|
|
|
15
24
|
const bridgeLog = log.child('shortcuts-official')
|
|
16
25
|
|
|
@@ -197,6 +206,7 @@ export function installLegacyKeys(
|
|
|
197
206
|
const onKey = (event: Event): void => {
|
|
198
207
|
if (disposed) return
|
|
199
208
|
const key = event as KeyboardEvent
|
|
209
|
+
if (key.isComposing || key.keyCode === 229 || key.defaultPrevented) return
|
|
200
210
|
const current = chords()
|
|
201
211
|
for (const def of defs) {
|
|
202
212
|
const chord = current[def.id]
|
|
@@ -206,7 +216,8 @@ export function installLegacyKeys(
|
|
|
206
216
|
bindings = parseChords(chord)
|
|
207
217
|
cache.set(chord, bindings)
|
|
208
218
|
}
|
|
209
|
-
if (
|
|
219
|
+
if (def.id === 'edrv.duplicateLine') continue // Monaco owns Ctrl+D; no global duplicate dispatch.
|
|
220
|
+
if (!matchEvent(key, bindings) || !shortcutAllowed(def.id, key.target) || !runner.isAvailable(def.id)) continue
|
|
210
221
|
key.preventDefault()
|
|
211
222
|
key.stopPropagation()
|
|
212
223
|
runner.run(def.id)
|
|
@@ -239,8 +250,8 @@ export function officialCommandOf(def: { id: string; label: string }, runner: Co
|
|
|
239
250
|
defaults: undefined,
|
|
240
251
|
regions: ['page', 'editable'],
|
|
241
252
|
modals: [],
|
|
242
|
-
resolve: () => {
|
|
243
|
-
if (!runner.isAvailable(def.id)) return { status: 'pass' }
|
|
253
|
+
resolve: (context) => {
|
|
254
|
+
if (context.modal !== null || !shortcutAllowed(def.id, context.target) || !runner.isAvailable(def.id)) return { status: 'pass' }
|
|
244
255
|
return {
|
|
245
256
|
status: 'handled',
|
|
246
257
|
run: () => {
|
|
@@ -101,6 +101,8 @@ export interface UiStateMirror {
|
|
|
101
101
|
hydrate(): Promise<number>
|
|
102
102
|
/** 立即上送待推送批次,返回最终丢弃的键数(0 = 全部落盘;供卸载与测试驱动)。 */
|
|
103
103
|
flush(): Promise<number>
|
|
104
|
+
/** Refresh-only strict resend; rejects on lost current state. */
|
|
105
|
+
flushStrict(): Promise<void>
|
|
104
106
|
/** 是否已完成一次水合。 */
|
|
105
107
|
hydrated(): boolean
|
|
106
108
|
/** 水合阶段是否已结束(成功或重试耗尽)。未结束时界面不应写入存档,否则会把空初值推给 host。 */
|
|
@@ -269,6 +271,19 @@ export function createUiStateMirror(deps: UiStateMirrorDeps): UiStateMirror {
|
|
|
269
271
|
return run
|
|
270
272
|
}
|
|
271
273
|
|
|
274
|
+
/** @private @author ddj 2026年10月12号 @returns Durable refresh snapshot. @throws When state is unreadable, unsettled or rejected by Host. @sideEffects Resends current local state after waiting for inflight writes. */
|
|
275
|
+
async function flushStrict(): Promise<void> {
|
|
276
|
+
if (!settleDone || !installed) throw new Error('界面状态镜像尚未就绪')
|
|
277
|
+
if (inflight) await inflight
|
|
278
|
+
// Read directly: the lenient key scanner intentionally hides storage errors.
|
|
279
|
+
for (let index = 0; index < storage.length; index++) {
|
|
280
|
+
const key = storage.key(index)
|
|
281
|
+
if (key && isMirroredKey(key)) queue(key, storage.getItem(key))
|
|
282
|
+
}
|
|
283
|
+
const lost = await flush()
|
|
284
|
+
if (lost > 0) throw new Error('界面状态保存失败(' + lost + ' 个键):' + lastError)
|
|
285
|
+
}
|
|
286
|
+
|
|
272
287
|
/** 回填本地缺失键(本地已有值一律保留)。 */
|
|
273
288
|
function applyHydration(snapshot: UiStateSnapshot): number {
|
|
274
289
|
let filled = 0
|
|
@@ -393,6 +408,7 @@ export function createUiStateMirror(deps: UiStateMirrorDeps): UiStateMirror {
|
|
|
393
408
|
},
|
|
394
409
|
hydrate,
|
|
395
410
|
flush,
|
|
411
|
+
flushStrict,
|
|
396
412
|
hydrated(): boolean {
|
|
397
413
|
return done
|
|
398
414
|
},
|
|
@@ -559,4 +575,10 @@ export function onUiStateReady(listener: () => void): () => void {
|
|
|
559
575
|
export async function flushUiState(): Promise<void> {
|
|
560
576
|
if (singleton) await singleton.flush()
|
|
561
577
|
}
|
|
578
|
+
|
|
579
|
+
/** @public @author ddj 2026年10月12号 @returns Durable page state. @throws If the mirror is unavailable or cannot persist. */
|
|
580
|
+
export async function flushPageState(): Promise<void> {
|
|
581
|
+
if (!singleton) throw new Error('界面状态镜像不可用')
|
|
582
|
+
await singleton.flushStrict()
|
|
583
|
+
}
|
|
562
584
|
// --endregion
|
|
@@ -14,6 +14,8 @@ import { bindPanelCommand } from '../panelCommands.js'
|
|
|
14
14
|
import { dbg, isDebug, rpc } from '../rpc.js'
|
|
15
15
|
import { emitFileChanged, emitRefresh } from '../events.js'
|
|
16
16
|
import { langOf, loadMonaco, snippetLanguageOf } from '../monaco/loader.js'
|
|
17
|
+
import { bindEditActions, runEditAction } from '../monaco/editActions.js'
|
|
18
|
+
import { addReloadGuard } from '../pageReload.js'
|
|
17
19
|
import { clampZoom, clearZoomMem, dataUrlOf, isImagePath, isSvgPath, recallZoom, rememberZoom, zoomKeyOf, zoomStepOf } from '../imagePreview.js'
|
|
18
20
|
import { isMarkdownPath } from '../markdownPreview.js'
|
|
19
21
|
import { setActiveEditorPath } from '../activePathStore.js'
|
|
@@ -565,6 +567,24 @@ export function EditorView(props) {
|
|
|
565
567
|
const saveViewState = (path) => viewKeeperRef.current?.save(path)
|
|
566
568
|
saveViewStateRef.current = saveViewState
|
|
567
569
|
|
|
570
|
+
React.useEffect(() => {
|
|
571
|
+
/** @private @author ddj 2026年10月12号 @returns Unsafe editor state without saving or discarding file content. */
|
|
572
|
+
function checkReload() {
|
|
573
|
+
if (Object.values(dirtyRef.current).some(Boolean)) return '存在未保存文件'
|
|
574
|
+
if (saveTimerRef.current.isPending() || savingRef.current.size) return '文件正在等待保存或保存中'
|
|
575
|
+
for (const ctl of pdfCtlRef.current.values()) {
|
|
576
|
+
if (ctl.isDirty?.()) return '存在未保存 PDF'
|
|
577
|
+
}
|
|
578
|
+
for (const path of tabPathsRef.current) {
|
|
579
|
+
if (readSync(scope, path)) return '存在外部文件冲突'
|
|
580
|
+
}
|
|
581
|
+
return null
|
|
582
|
+
}
|
|
583
|
+
/** @private @author ddj 2026年10月12号 Captures view state only; never flushes file auto-save. */
|
|
584
|
+
function prepareReload() { saveViewStateRef.current?.() }
|
|
585
|
+
return addReloadGuard({ check: checkReload, prepare: prepareReload })
|
|
586
|
+
}, [scope])
|
|
587
|
+
|
|
568
588
|
/** @private @author ddj 2026年10月09号 ref 脱离与 effect 卸载共用:保存视图/查找后销毁,重复调用无副作用。 */
|
|
569
589
|
const disposeEditor = React.useCallback(() => {
|
|
570
590
|
viewKeeperRef.current?.dispose()
|
|
@@ -1690,6 +1710,9 @@ export function EditorView(props) {
|
|
|
1690
1710
|
['edrv.command.svnTortoiseDiff', () => runSvnTortoise('diff', activeRef.current)],
|
|
1691
1711
|
['edrv.command.svnTortoiseBlame', () => runSvnTortoise('blame', activeRef.current)],
|
|
1692
1712
|
['edrv.command.svnTortoiseRevert', () => runSvnTortoise('revert', activeRef.current)],
|
|
1713
|
+
['edrv:reload-status', (event) => setStatus(event.detail)],
|
|
1714
|
+
['edrv.command.toggleComment', () => runEditAction(editorRef.current, monacoRef.current, 'editor.action.commentLine')],
|
|
1715
|
+
['edrv.command.duplicateLine', () => runEditAction(editorRef.current, monacoRef.current, 'editor.action.copyLinesDownAction')],
|
|
1693
1716
|
// 转到行:转发 Monaco 原生 action(widget 本体/占位/跳转全原生,插件只补键位与命令栏入口)
|
|
1694
1717
|
['edrv.command.goToLine', () => {
|
|
1695
1718
|
const ed = editorRef.current
|
|
@@ -1702,7 +1725,7 @@ export function EditorView(props) {
|
|
|
1702
1725
|
const onCommand = (event) => {
|
|
1703
1726
|
const action = byName.get(event.type)
|
|
1704
1727
|
if (!action) return
|
|
1705
|
-
try { action() } catch (error) { dbg(sessionId, '指令 ' + event.type + ' 失败:' + String(error)) }
|
|
1728
|
+
try { action(event) } catch (error) { dbg(sessionId, '指令 ' + event.type + ' 失败:' + String(error)) }
|
|
1706
1729
|
}
|
|
1707
1730
|
for (const [eventName] of handlers) window.addEventListener(eventName, onCommand)
|
|
1708
1731
|
return () => {
|
|
@@ -2157,6 +2180,7 @@ export function EditorView(props) {
|
|
|
2157
2180
|
// AI 内联补全(ghost text):开关经 edrv.ai.configGet 拉取;provider 见 client/ai/inlineProvider
|
|
2158
2181
|
inlineSuggest: { enabled: true },
|
|
2159
2182
|
})
|
|
2183
|
+
bindEditActions(ed, m)
|
|
2160
2184
|
// 保存改由窗口级快捷键监听执行(键位可配置;见上方 edrv.save 监听)
|
|
2161
2185
|
ed.onDidChangeModelContent(() => {
|
|
2162
2186
|
if (!ed.getModel() || programmaticRef.current) return
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* 作者 ddj 2026年09月10号
|
|
8
8
|
*/
|
|
9
9
|
import { panelAvailable, runPanelCommand } from '../panelCommands.js'
|
|
10
|
+
import { pageKeysAllowed } from '../pageReload.js'
|
|
10
11
|
import { hasEditorModel, hasOpenTabs } from '../editorModelState.js'
|
|
11
12
|
import { activeEditorPath } from '../activePathStore.js'
|
|
12
13
|
import { isMarkdownPath } from '../markdownPreview.js'
|
|
@@ -195,6 +196,21 @@ function svnEventOf(action: SvnActionDef): string {
|
|
|
195
196
|
* @author ddj 2026年09月10号 / 2026年10月
|
|
196
197
|
*/
|
|
197
198
|
export const EDITOR_COMMANDS: readonly CommandDef[] = [
|
|
199
|
+
{
|
|
200
|
+
id: 'edrv.toggleComment', label: '切换行注释', category: '编辑', order: 20,
|
|
201
|
+
available: needsModel,
|
|
202
|
+
run: () => emit('toggleComment'),
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 'edrv.duplicateLine', label: '向下复制行(Ctrl+D,编辑器本地)', category: '编辑', order: 30,
|
|
206
|
+
available: needsModel,
|
|
207
|
+
run: () => emit('duplicateLine'),
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: 'edrv.reloadWindow', label: '刷新当前界面(不重启 Host)', category: '视图', order: 40,
|
|
211
|
+
available: pageKeysAllowed,
|
|
212
|
+
run: () => emit('reloadWindow'),
|
|
213
|
+
},
|
|
198
214
|
{
|
|
199
215
|
id: 'edrv.save', label: '保存文件', category: '文件', order: 10,
|
|
200
216
|
available: needsModel,
|
package/src/compat.ts
CHANGED
|
@@ -132,11 +132,6 @@ export function detectGuards(ctx: Ctx): CompatAdapter[] {
|
|
|
132
132
|
]
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
/** 已实测覆盖的最高 DSH 版本(适配矩阵上界,超过则提示,见 buildReport)。
|
|
136
|
-
* 0.1.7-rc.2 已在官方已签名打包 Desktop 上实机装配(认证路由、同源 RPC、项目 MCP
|
|
137
|
-
* 与私有打开队列均生效);更高版本仍按能力探测运行并提示。 */
|
|
138
|
-
const TESTED_DSH_MAX = '0.1.7-rc.2'
|
|
139
|
-
|
|
140
135
|
/** Config volatile 标记起效的版本线(此前设置走 section 安装,与 volatile 无关)。 */
|
|
141
136
|
const CONFIG_VOLATILE_MIN = '0.1.7-alpha.1'
|
|
142
137
|
|
|
@@ -193,10 +188,6 @@ export async function buildReport(
|
|
|
193
188
|
const strategy = settingsInstallStrategy()
|
|
194
189
|
if (strategy === 'none') warnings.push('设置 section 安装不可用:' + settingsInstallNote())
|
|
195
190
|
const parsed = parseDshVersion(dshVersion)
|
|
196
|
-
const testedMax = parseDshVersion(TESTED_DSH_MAX)
|
|
197
|
-
if (parsed && testedMax && compareDshVersions(parsed, testedMax) > 0) {
|
|
198
|
-
warnings.push('DSH ' + dshVersion + ' 高于已实测版本(' + TESTED_DSH_MAX + '):设置 API 按能力探测运行,异常时请回报适配矩阵')
|
|
199
|
-
}
|
|
200
191
|
// 0.1.7 线上 Config 未完成 volatile 标记 = 设置页不可用(ns 不下发),显式告警而非静默
|
|
201
192
|
const volatileState = configVolatileState()
|
|
202
193
|
const volatileMin = parseDshVersion(CONFIG_VOLATILE_MIN)
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
/** 命令 id → 默认键位(可为多候选)。命令目录以此为准,新增命令只需加一项。 */
|
|
14
14
|
export const KEYBINDING_DEFAULTS: Record<string, string> = {
|
|
15
15
|
'edrv.save': 'Ctrl+S',
|
|
16
|
+
'edrv.toggleComment': 'Ctrl+Alt+/',
|
|
17
|
+
'edrv.duplicateLine': '', // Ctrl+D is Monaco-local, never a page-wide binding.
|
|
18
|
+
'edrv.reloadWindow': '', // Desktop-only defaults are declared below.
|
|
16
19
|
'edrv.quickOpen': 'Ctrl+P',
|
|
17
20
|
'edrv.toggleSidebar': 'Ctrl+B',
|
|
18
21
|
'edrv.searchInFiles': 'Ctrl+Shift+F',
|
|
@@ -206,6 +209,19 @@ function bind(code: string, modifiers: OfficialModifier[]): OfficialBinding {
|
|
|
206
209
|
*/
|
|
207
210
|
export const SHORTCUT_PROFILES: Record<string, ShortcutProfiles> = {
|
|
208
211
|
'edrv.toggleSidebar': {},
|
|
212
|
+
'edrv.duplicateLine': {},
|
|
213
|
+
'edrv.toggleComment': {
|
|
214
|
+
'desktop:macos': bind('Slash', ['primary', 'alt']),
|
|
215
|
+
'desktop:windows': bind('Slash', ['primary', 'alt']),
|
|
216
|
+
'desktop:linux': bind('Slash', ['primary', 'alt']),
|
|
217
|
+
'web:macos': bind('Slash', ['primary', 'alt']),
|
|
218
|
+
'web:windows': bind('Slash', ['primary', 'alt']),
|
|
219
|
+
},
|
|
220
|
+
'edrv.reloadWindow': {
|
|
221
|
+
'desktop:macos': bind('KeyR', ['primary', 'shift']),
|
|
222
|
+
'desktop:windows': bind('KeyR', ['primary', 'shift']),
|
|
223
|
+
'desktop:linux': bind('KeyR', ['primary', 'shift']),
|
|
224
|
+
},
|
|
209
225
|
'edrv.save': {
|
|
210
226
|
'desktop:macos': bind('KeyS', ['primary']),
|
|
211
227
|
'desktop:windows': bind('KeyS', ['primary']),
|