dsh-vscode-mode 0.14.8 → 0.14.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.14.8",
3
+ "version": "0.14.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": [
@@ -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 实例,但仍是「有页签可关」。
@@ -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 { flushUiState, hydrateUiState, installUiStatePersist, onUiStateReady, reportUiStateEvent, uiStateSettled } from './state/uiStatePersist.js'
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 (!matchEvent(key, bindings) || !runner.isAvailable(def.id)) continue
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,
@@ -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']),