Przeglądaj źródła

Enhance/shortcuts (#9803)

* refactor(shortcuts): simplify to build handler category map

* fix(shortcuts): redundant re-mount for the pdf shortcuts

* refactor(shortcuts): simplify names

* refactor(shortcuts): simplify user keynames

* fix(shortcuts): persist inited state for dev mode

* refactor(shortcuts): simplify handlers installation

* refactor(shortcuts): optimize shortcuts mixin

* fix: incorrect function ref

* refactor(shortcuts): shortcuts mixin

* fix(shortcuts): incorrect initialization for the pdf shortcut handler

* refactor(shortcuts): optimize binding keys map

* refactor(shortcuts): optimize shortcuts conflicts detection

* refactor(shortcuts): optimize binding ids map

* refactor(shortcuts): WIP the new keymap page

* refactor(shortcuts): WIP the new keymap related components

* feat(shortcuts): WIP fuzzy search for the shortcuts

* refactor(shortcuts): WIP the new keymap related components

* feat(shortcuts): WIP the new shorcuts record component

* feat(shortcuts): WIP the new shorcuts record component

* feat(shortcuts): WIP check shortcut conflicts component

* feat(shortcuts): WIP the new shorcuts record component

* refactor(shortcuts): WIP persist user shortcuts

* fix(shortcuts): detection for the conflicts

* feat(shortcuts): WIP detection for the conflicts

* feat(shortcuts): WIP persist user shortcuts

* refactor(shortcuts): add unit tests

* enhance(ux): search pane for the shortcuts

* feat(shortcuts): remove the existent shortcut item

* feat(shortcuts): fold/unfold categories

* feat(shortcuts): add shortcuts filters

* enhance(shortcuts): resove binding map description

* enhance(shortcuts): reactive category shortcuts

* enhance(shortcuts): register api for plugins

* feat(shortcuts): add keyboard shortcuts filters

* feat(shortcuts): impl keyboard shortcuts filters

* enhance(shortcuts): leader keys for the shortcut conflicts detection

* enhance(tests): leader keys conflicts for the shortucts

* enhance(shortcuts): parse conflicts from current binding list

* enhance(ui): polish the component of the restore shortcut action

* enhance(shortcuts): get conflicts with specific handler id

* enhance(shortcuts): polish the confilts component

* enhance(shortcuts): polish keymap conflicts component

* enhance(shortcuts): ux for handling shorcuts conflicts

* enhance(ui): polish notifications cp

* fix(shortcuts): remove reduplicate shortcuts for category

* enhance(shortcuts): polish ux for handling shorcuts conflicts

* chore(plugin): build libs core

* enhance(plugin): support shortcut command lifecycle hooks

* enhance(plugin): support shortcut command lifecycle hooks

* chore(plugin): build libs core

* enhance(shortcuts): support shortcuts saved to global config

* enhance(shortcuts): support shortcuts be saved to global config

* feat(shortcuts): support keymap manager to global settings

* enhance(shortcuts): shortcut to open keymap settings

* fix(units): tests

* fix: lints

* enhance(shortcuts): unlisten all shortcuts

* fix: lints

* fix: lints

* fix(units): tests

* fix(units): tests

* fix(units): tests

* enhance(shortcuts): unlisten/listen all shortcuts

* enhance(shortcuts): polish conflicts component

* fix(ui): modal size

* fix(ui): modal panel container

* enhance(shortcuts): i18n

* enhance(ui): layout of the shortcuts recorder component

* fix(lint): i18n

* enhance(ui): keyboard icon for the keymap settings tab

* fix(shortcuts): incorrect filters for the collaspsed shortcuts

* enhance(ui): polish details for the keymap settings

* enhance(ui): polish details for the keymap settings

* fix(shortcuts): get shortcut description error when the associated handler-id not exist

* fix(ui): the shortcut disabled label overlaps with section headers.

* refactor(shortcuts): names

* enhance(ui): filter icons
Charlie 2 lat temu
rodzic
commit
6d6da2046c
44 zmienionych plików z 2384 dodań i 1346 usunięć
  1. 10 5
      libs/src/LSPlugin.caller.ts
  2. 39 30
      libs/src/LSPlugin.core.ts
  3. 55 32
      libs/src/LSPlugin.ts
  4. 113 99
      libs/src/LSPlugin.user.ts
  5. 42 37
      libs/src/helpers.ts
  6. 2 1
      libs/src/modules/LSPlugin.Experiments.ts
  7. 34 36
      libs/src/modules/LSPlugin.Request.ts
  8. 48 51
      libs/src/modules/LSPlugin.Search.ts
  9. 1 1
      libs/src/modules/LSPlugin.Storage.ts
  10. 3 1
      libs/src/postmate/index.ts
  11. 0 0
      resources/js/lsplugin.core.js
  12. 2 2
      src/main/frontend/components/command_palette.cljs
  13. 2 2
      src/main/frontend/components/plugins.css
  14. 1 1
      src/main/frontend/components/plugins_settings.cljs
  15. 32 5
      src/main/frontend/components/settings.cljs
  16. 40 52
      src/main/frontend/components/settings.css
  17. 4 15
      src/main/frontend/components/shortcut.cljs
  18. 165 0
      src/main/frontend/components/shortcut.css
  19. 476 0
      src/main/frontend/components/shortcut2.cljs
  20. 1 1
      src/main/frontend/components/theme.cljs
  21. 1 1
      src/main/frontend/components/whiteboard.cljs
  22. 9 4
      src/main/frontend/extensions/pdf/core.cljs
  23. 1 1
      src/main/frontend/extensions/srs.cljs
  24. 3 2
      src/main/frontend/handler/command_palette.cljs
  25. 3 0
      src/main/frontend/handler/config.cljs
  26. 4 7
      src/main/frontend/handler/events.cljs
  27. 18 1
      src/main/frontend/handler/global_config.cljs
  28. 5 3
      src/main/frontend/handler/notification.cljs
  29. 12 3
      src/main/frontend/handler/plugin.cljs
  30. 26 24
      src/main/frontend/mixins.cljs
  31. 673 696
      src/main/frontend/modules/shortcut/config.cljs
  32. 158 74
      src/main/frontend/modules/shortcut/core.cljs
  33. 173 73
      src/main/frontend/modules/shortcut/data_helper.cljs
  34. 58 0
      src/main/frontend/modules/shortcut/utils.cljs
  35. 0 5
      src/main/frontend/routes.cljs
  36. 30 12
      src/main/frontend/state.cljs
  37. 7 6
      src/main/frontend/ui.cljs
  38. 1 1
      src/main/frontend/ui/date_picker.cljs
  39. 0 13
      src/main/frontend/util.cljc
  40. 4 4
      src/main/logseq/api.cljs
  41. 17 1
      src/resources/dicts/en.edn
  42. 18 0
      src/resources/dicts/zh-cn.edn
  43. 49 0
      src/test/frontend/modules/shortcut/core_test.cljs
  44. 44 44
      src/test/frontend/util_test.cljs

+ 10 - 5
libs/src/LSPlugin.caller.ts

@@ -243,18 +243,23 @@ class LSPluginCaller extends EventEmitter {
         let { width, height, left, top, vw, vh } = mainLayoutInfo
 
         left = Math.max(left, 0)
-        left = (typeof vw === 'number') ?
-          `${Math.min(left * 100 / vw, 99)}%` : `${left}px`
+        left =
+          typeof vw === 'number'
+            ? `${Math.min((left * 100) / vw, 99)}%`
+            : `${left}px`
 
         // 45 is height of headbar
         top = Math.max(top, 45)
-        top = (typeof vh === 'number') ?
-          `${Math.min(top * 100 / vh, 99)}%` : `${top}px`
+        top =
+          typeof vh === 'number'
+            ? `${Math.min((top * 100) / vh, 99)}%`
+            : `${top}px`
 
         Object.assign(cnt.style, {
           width: width + 'px',
           height: height + 'px',
-          left, top
+          left,
+          top,
         })
       }
     } catch (e) {

+ 39 - 30
libs/src/LSPlugin.core.ts

@@ -395,11 +395,9 @@ class ExistedImportedPluginPackageError extends Error {
 /**
  * Host plugin for local
  */
-class PluginLocal extends EventEmitter<'loaded'
-  | 'unloaded'
-  | 'beforeunload'
-  | 'error'
-  | string> {
+class PluginLocal extends EventEmitter<
+  'loaded' | 'unloaded' | 'beforeunload' | 'error' | string
+> {
   private _sdk: Partial<PluginLocalSDKMetadata> = {}
   private _disposes: Array<() => Promise<any>> = []
   private _id: PluginLocalIdentity
@@ -534,7 +532,7 @@ class PluginLocal extends EventEmitter<'loaded'
     const localRoot = (this._localRoot = safetyPathNormalize(url))
     const logseq: Partial<LSPluginPkgConfig> = pkg.logseq || {}
 
-      // Pick legal attrs
+    // Pick legal attrs
     ;[
       'name',
       'author',
@@ -594,7 +592,9 @@ class PluginLocal extends EventEmitter<'loaded'
     // Validate id
     const { registeredPlugins, isRegistering } = this._ctx
     if (isRegistering && registeredPlugins.has(this.id)) {
-      throw new ExistedImportedPluginPackageError('Registered plugin package Error')
+      throw new ExistedImportedPluginPackageError(
+        'Registered plugin package Error'
+      )
     }
 
     return async () => {
@@ -642,10 +642,10 @@ class PluginLocal extends EventEmitter<'loaded'
     <meta charset="UTF-8">
     <title>logseq plugin entry</title>
     ${
-        IS_DEV
-          ? `<script src="${sdkPathRoot}/lsplugin.user.js?v=${tag}"></script>`
-          : `<script src="https://cdn.jsdelivr.net/npm/@logseq/libs/dist/lsplugin.user.min.js?v=${tag}"></script>`
-      }
+      IS_DEV
+        ? `<script src="${sdkPathRoot}/lsplugin.user.js?v=${tag}"></script>`
+        : `<script src="https://cdn.jsdelivr.net/npm/@logseq/libs/dist/lsplugin.user.min.js?v=${tag}"></script>`
+    }
     
   </head>
   <body>
@@ -1103,7 +1103,8 @@ class LSPluginCore
     | 'beforereload'
     | 'reloaded'
   >
-  implements ILSPluginThemeManager {
+  implements ILSPluginThemeManager
+{
   private _isRegistering = false
   private _readyIndicator?: DeferredActor
   private readonly _hostMountedActor: DeferredActor = deferred()
@@ -1117,8 +1118,10 @@ class LSPluginCore
     externals: [],
   }
   private readonly _registeredThemes = new Map<PluginLocalIdentity, Theme[]>()
-  private readonly _registeredPlugins = new Map<PluginLocalIdentity,
-    PluginLocal>()
+  private readonly _registeredPlugins = new Map<
+    PluginLocalIdentity,
+    PluginLocal
+  >()
   private _currentTheme: {
     pid: PluginLocalIdentity
     opt: Theme | LegacyTheme
@@ -1194,14 +1197,18 @@ class LSPluginCore
       return
     }
 
-    const perfTable = new Map<string,
-      { o: PluginLocal; s: number; e: number }>()
+    const perfTable = new Map<
+      string,
+      { o: PluginLocal; s: number; e: number }
+    >()
     const debugPerfInfo = () => {
       const data: any = Array.from(perfTable.values()).reduce((ac, it) => {
         const { id, options, status, disabled } = it.o
 
-        if (disabled !== true &&
-          (options.entry || (!options.name && !options.entry))) {
+        if (
+          disabled !== true &&
+          (options.entry || (!options.name && !options.entry))
+        ) {
           ac[id] = {
             name: options.name,
             entry: options.entry,
@@ -1234,17 +1241,19 @@ class LSPluginCore
       // valid externals
       if (externals?.size) {
         try {
-          const validatedExternals: Record<string, boolean> = await invokeHostExportedApi(
-            'validate_external_plugins', [...externals]
-          )
+          const validatedExternals: Record<string, boolean> =
+            await invokeHostExportedApi('validate_external_plugins', [
+              ...externals,
+            ])
 
-          externals = new Set([...Object.entries(validatedExternals)].reduce(
-            (a, [k, v]) => {
+          externals = new Set(
+            [...Object.entries(validatedExternals)].reduce((a, [k, v]) => {
               if (v) {
                 a.push(k)
               }
               return a
-            }, []))
+            }, [])
+          )
         } catch (e) {
           console.error('[validatedExternals Error]', e)
         }
@@ -1557,12 +1566,12 @@ class LSPluginCore
       await this.saveUserPreferences(
         theme.mode
           ? {
-            themes: {
-              ...this._userPreferences.themes,
-              mode: theme.mode,
-              [theme.mode]: theme,
-            },
-          }
+              themes: {
+                ...this._userPreferences.themes,
+                mode: theme.mode,
+                [theme.mode]: theme,
+              },
+            }
           : { theme: theme }
       )
     }

+ 55 - 32
libs/src/LSPlugin.ts

@@ -6,7 +6,8 @@ import { LSPluginExperiments } from './modules/LSPlugin.Experiments'
 import { IAsyncStorage, LSPluginFileStorage } from './modules/LSPlugin.Storage'
 import { LSPluginRequest } from './modules/LSPlugin.Request'
 
-export type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
+export type WithOptional<T, K extends keyof T> = Omit<T, K> &
+  Partial<Pick<T, K>>
 
 export type PluginLocalIdentity = string
 
@@ -297,7 +298,12 @@ export type ExternalCommandType =
 export type UserProxyTags = 'app' | 'editor' | 'db' | 'git' | 'ui' | 'assets'
 
 export type SearchIndiceInitStatus = boolean
-export type SearchBlockItem = { id: EntityID, uuid: BlockIdentity, content: string, page: EntityID }
+export type SearchBlockItem = {
+  id: EntityID
+  uuid: BlockIdentity
+  content: string
+  page: EntityID
+}
 export type SearchPageItem = string
 export type SearchFileItem = string
 
@@ -309,21 +315,23 @@ export interface IPluginSearchServiceHooks {
     graph: string,
     key: string,
     opts: Partial<{ limit: number }>
-  ) =>
-    Promise<{
-      graph: string,
-      key: string,
-      blocks?: Array<Partial<SearchBlockItem>>,
-      pages?: Array<SearchPageItem>,
-      files?: Array<SearchFileItem>
-    }>
+  ) => Promise<{
+    graph: string
+    key: string
+    blocks?: Array<Partial<SearchBlockItem>>
+    pages?: Array<SearchPageItem>
+    files?: Array<SearchFileItem>
+  }>
 
   onIndiceInit: (graph: string) => Promise<SearchIndiceInitStatus>
   onIndiceReset: (graph: string) => Promise<void>
-  onBlocksChanged: (graph: string, changes: {
-    added: Array<SearchBlockItem>,
-    removed: Array<EntityID>
-  }) => Promise<void>
+  onBlocksChanged: (
+    graph: string,
+    changes: {
+      added: Array<SearchBlockItem>
+      removed: Array<EntityID>
+    }
+  ) => Promise<void>
   onGraphRemoved: (graph: string, opts?: {}) => Promise<any>
 }
 
@@ -372,8 +380,14 @@ export interface IAppProxy {
    * @param action
    */
   registerCommandShortcut: (
-    keybinding: SimpleCommandKeybinding,
-    action: SimpleCommandCallback
+    keybinding: SimpleCommandKeybinding | string,
+    action: SimpleCommandCallback,
+    opts?: Partial<{
+      key: string
+      label: string
+      desc: string
+      extras: Record<string, any>
+    }>
   ) => void
 
   /**
@@ -392,10 +406,7 @@ export interface IAppProxy {
    * @param type `xx-plugin-id.commands.xx-key`, `xx-plugin-id.models.xx-key`
    * @param args
    */
-  invokeExternalPlugin: (
-    type: string,
-    ...args: Array<any>
-  ) => Promise<unknown>
+  invokeExternalPlugin: (type: string, ...args: Array<any>) => Promise<unknown>
 
   /**
    * @added 0.0.13
@@ -452,7 +463,11 @@ export interface IAppProxy {
   // templates
   getTemplate: (name: string) => Promise<BlockEntity | null>
   existTemplate: (name: string) => Promise<Boolean>
-  createTemplate: (target: BlockUUID, name: string, opts?: { overwrite: boolean }) => Promise<any>
+  createTemplate: (
+    target: BlockUUID,
+    name: string,
+    opts?: { overwrite: boolean }
+  ) => Promise<any>
   removeTemplate: (name: string) => Promise<any>
   insertTemplate: (target: BlockUUID, name: string) => Promise<any>
 
@@ -495,15 +510,21 @@ export interface IAppProxy {
   onCurrentGraphChanged: IUserHook
   onGraphAfterIndexed: IUserHook<{ repo: string }>
   onThemeModeChanged: IUserHook<{ mode: 'dark' | 'light' }>
-  onThemeChanged: IUserHook<Partial<{ name: string, mode: string, pid: string, url: string }>>
+  onThemeChanged: IUserHook<
+    Partial<{ name: string; mode: string; pid: string; url: string }>>
   onTodayJournalCreated: IUserHook<{ title: string }>
+  onBeforeCommandInvoked: (condition: ExternalCommandType | string, callback: (e: IHookEvent) => void) => IUserOffHook
+  onAfterCommandInvoked: (condition: ExternalCommandType | string, callback: (e: IHookEvent) => void) => IUserOffHook
 
   /**
    * provide ui slot to specific block with UUID
    *
    * @added 0.0.13
    */
-  onBlockRendererSlotted: IUserConditionSlotHook<BlockUUID, Omit<BlockEntity, 'children' | 'page'>>
+  onBlockRendererSlotted: IUserConditionSlotHook<
+    BlockUUID,
+    Omit<BlockEntity, 'children' | 'page'>
+  >
 
   /**
    * provide ui slot to block `renderer` macro for `{{renderer arg1, arg2}}`
@@ -690,7 +711,7 @@ export interface IEditorProxy extends Record<string, any> {
   insertBatchBlock: (
     srcBlock: BlockIdentity,
     batch: IBatchBlock | Array<IBatchBlock>,
-    opts?: Partial<{ before: boolean; sibling: boolean, keepUUID: boolean }>
+    opts?: Partial<{ before: boolean; sibling: boolean; keepUUID: boolean }>
   ) => Promise<Array<BlockEntity> | null>
 
   updateBlock: (
@@ -896,14 +917,16 @@ export interface IAssetsProxy {
    * @added 0.0.2
    * @param exts
    */
-  listFilesOfCurrentGraph(exts?: string | string[]): Promise<Array<{
-    path: string
-    size: number
-    accessTime: number
-    modifiedTime: number
-    changeTime: number
-    birthTime: number
-  }>>
+  listFilesOfCurrentGraph(exts?: string | string[]): Promise<
+    Array<{
+      path: string
+      size: number
+      accessTime: number
+      modifiedTime: number
+      changeTime: number
+      birthTime: number
+    }>
+  >
 
   /**
    * @example https://github.com/logseq/logseq/pull/6488

+ 113 - 99
libs/src/LSPlugin.user.ts

@@ -35,7 +35,8 @@ import {
   BlockEntity,
   IDatom,
   IAssetsProxy,
-  AppInfo, IPluginSearchServiceHooks,
+  AppInfo,
+  IPluginSearchServiceHooks,
 } from './LSPlugin'
 import Debug from 'debug'
 import * as CSS from 'csstype'
@@ -52,8 +53,7 @@ declare global {
   }
 }
 
-type callableMethods =
-  keyof typeof callableAPIs | string // host exported SDK apis & host platform related apis
+type callableMethods = keyof typeof callableAPIs | string // host exported SDK apis & host platform related apis
 
 const PROXY_CONTINUE = Symbol.for('proxy-continue')
 const debug = Debug('LSPlugin:user')
@@ -64,7 +64,7 @@ const logger = new PluginLogger('', { console: true })
  * @param opts
  * @param action
  */
-function registerSimpleCommand(
+function registerSimpleCommand (
   this: LSPluginUser,
   type: string,
   opts: {
@@ -91,13 +91,16 @@ function registerSimpleCommand(
     args: [
       this.baseInfo.id,
       // [cmd, action]
-      [{ key, label, type, desc, keybinding, extras }, ['editor/hook', eventKey]],
+      [
+        { key, label, type, desc, keybinding, extras },
+        ['editor/hook', eventKey],
+      ],
       palette,
     ],
   })
 }
 
-function shouldValidUUID(uuid: string) {
+function shouldValidUUID (uuid: string) {
   if (!isValidUUID(uuid)) {
     logger.error(`#${uuid} is not a valid UUID string.`)
     return false
@@ -106,7 +109,7 @@ function shouldValidUUID(uuid: string) {
   return true
 }
 
-function checkEffect(p: LSPluginUser) {
+function checkEffect (p: LSPluginUser) {
   return p && (p.baseInfo?.effect || !p.baseInfo?.iir)
 }
 
@@ -114,10 +117,7 @@ let _appBaseInfo: AppInfo = null
 let _searchServices: Map<string, LSPluginSearchService> = new Map()
 
 const app: Partial<IAppProxy> = {
-  async getInfo(
-    this: LSPluginUser,
-    key
-  ) {
+  async getInfo (this: LSPluginUser, key) {
     if (!_appBaseInfo) {
       _appBaseInfo = await this._execCallableAPIAsync('get-app-info')
     }
@@ -126,7 +126,7 @@ const app: Partial<IAppProxy> = {
 
   registerCommand: registerSimpleCommand,
 
-  registerSearchService<T extends IPluginSearchServiceHooks>(
+  registerSearchService<T extends IPluginSearchServiceHooks> (
     this: LSPluginUser,
     s: T
   ) {
@@ -137,7 +137,7 @@ const app: Partial<IAppProxy> = {
     _searchServices.set(s.name, new LSPluginSearchService(this, s))
   },
 
-  registerCommandPalette(
+  registerCommandPalette (
     opts: { key: string; label: string; keybinding?: SimpleCommandKeybinding },
     action: SimpleCommandCallback
   ) {
@@ -152,10 +152,23 @@ const app: Partial<IAppProxy> = {
     )
   },
 
-  registerCommandShortcut(
-    keybinding: SimpleCommandKeybinding,
-    action: SimpleCommandCallback
+  registerCommandShortcut (
+    keybinding: SimpleCommandKeybinding | string,
+    action: SimpleCommandCallback,
+    opts: Partial<{
+      key: string
+      label: string
+      desc: string
+      extras: Record<string, any>
+    }> = {}
   ) {
+    if (typeof keybinding == 'string') {
+      keybinding = {
+        mode: 'global',
+        binding: keybinding,
+      }
+    }
+
     const { binding } = keybinding
     const group = '$shortcut$'
     const key = group + safeSnakeCase(binding)
@@ -163,12 +176,12 @@ const app: Partial<IAppProxy> = {
     return registerSimpleCommand.call(
       this,
       group,
-      { key, palette: false, keybinding },
+      { ...opts, key, palette: false, keybinding },
       action
     )
   },
 
-  registerUIItem(
+  registerUIItem (
     type: 'toolbar' | 'pagebar',
     opts: { key: string; template: string }
   ) {
@@ -181,7 +194,7 @@ const app: Partial<IAppProxy> = {
     })
   },
 
-  registerPageMenuItem(
+  registerPageMenuItem (
     this: LSPluginUser,
     tag: string,
     action: (e: IHookEvent & { page: string }) => void
@@ -205,9 +218,7 @@ const app: Partial<IAppProxy> = {
     )
   },
 
-  onBlockRendererSlotted(
-    uuid,
-    callback: (payload: any) => void) {
+  onBlockRendererSlotted (uuid, callback: (payload: any) => void) {
     if (!shouldValidUUID(uuid)) return
 
     const pid = this.baseInfo.id
@@ -222,11 +233,7 @@ const app: Partial<IAppProxy> = {
     }
   },
 
-  invokeExternalPlugin(
-    this: LSPluginUser,
-    type: string,
-    ...args: Array<any>
-  ) {
+  invokeExternalPlugin (this: LSPluginUser, type: string, ...args: Array<any>) {
     type = type?.trim()
     if (!type) return
     let [pid, group] = type.split('.')
@@ -240,11 +247,14 @@ const app: Partial<IAppProxy> = {
     }
     return this._execCallableAPIAsync(
       'invoke_external_plugin_cmd',
-      pid, group.toLowerCase(), key, args
+      pid,
+      group.toLowerCase(),
+      key,
+      args
     )
   },
 
-  setFullScreen(flag) {
+  setFullScreen (flag) {
     const sf = (...args) => this._callWin('setFullScreen', ...args)
 
     if (flag === 'toggle') {
@@ -254,17 +264,17 @@ const app: Partial<IAppProxy> = {
     } else {
       flag ? sf(true) : sf()
     }
-  }
+  },
 }
 
 let registeredCmdUid = 0
 
 const editor: Partial<IEditorProxy> = {
-  newBlockUUID(this: LSPluginUser): Promise<string> {
+  newBlockUUID (this: LSPluginUser): Promise<string> {
     return this._execCallableAPIAsync('new_block_uuid')
   },
 
-  registerSlashCommand(
+  registerSlashCommand (
     this: LSPluginUser,
     tag: string,
     actions: BlockCommandCallback | Array<SlashCommandAction>
@@ -312,7 +322,7 @@ const editor: Partial<IEditorProxy> = {
     })
   },
 
-  registerBlockContextMenuItem(
+  registerBlockContextMenuItem (
     this: LSPluginUser,
     label: string,
     action: BlockCommandCallback
@@ -335,11 +345,12 @@ const editor: Partial<IEditorProxy> = {
     )
   },
 
-  registerHighlightContextMenuItem(
+  registerHighlightContextMenuItem (
     this: LSPluginUser,
     label: string,
     action: SimpleCommandCallback,
-    opts?: { clearSelection: boolean }) {
+    opts?: { clearSelection: boolean }
+  ) {
     if (typeof action !== 'function') {
       return false
     }
@@ -353,13 +364,13 @@ const editor: Partial<IEditorProxy> = {
       {
         key,
         label,
-        extras: opts
+        extras: opts,
       },
       action
     )
   },
 
-  scrollToBlockInPage(
+  scrollToBlockInPage (
     this: LSPluginUser,
     pageName: BlockPageName,
     blockId: BlockIdentity,
@@ -371,11 +382,11 @@ const editor: Partial<IEditorProxy> = {
     } else {
       this.App.pushState('page', { name: pageName }, { anchor })
     }
-  }
+  },
 }
 
 const db: Partial<IDBProxy> = {
-  onBlockChanged(
+  onBlockChanged (
     this: LSPluginUser,
     uuid: BlockUUID,
     callback: (
@@ -405,7 +416,7 @@ const db: Partial<IDBProxy> = {
     }
   },
 
-  datascriptQuery<T = any>(
+  datascriptQuery<T = any> (
     this: LSPluginUser,
     query: string,
     ...inputs: Array<any>
@@ -413,16 +424,13 @@ const db: Partial<IDBProxy> = {
     // force remove proxy ns flag `db`
     inputs.pop()
 
-    if (inputs?.some(it => (typeof it === 'function'))) {
+    if (inputs?.some((it) => typeof it === 'function')) {
       const host = this.Experiments.ensureHostScope()
       return host.logseq.api.datascript_query(query, ...inputs)
     }
 
-    return this._execCallableAPIAsync(
-      `datascript_query`,
-      ...[query, ...inputs]
-    )
-  }
+    return this._execCallableAPIAsync(`datascript_query`, ...[query, ...inputs])
+  },
 }
 
 const git: Partial<IGitProxy> = {}
@@ -430,13 +438,9 @@ const git: Partial<IGitProxy> = {}
 const ui: Partial<IUIProxy> = {}
 
 const assets: Partial<IAssetsProxy> = {
-  makeSandboxStorage(
-    this: LSPluginUser
-  ): IAsyncStorage {
-    return new LSPluginFileStorage(
-      this, { assets: true }
-    )
-  }
+  makeSandboxStorage (this: LSPluginUser): IAsyncStorage {
+    return new LSPluginFileStorage(this, { assets: true })
+  },
 }
 
 type uiState = {
@@ -483,7 +487,7 @@ export class LSPluginUser
    * @param _baseInfo
    * @param _caller
    */
-  constructor(
+  constructor (
     private _baseInfo: LSPluginBaseInfo,
     private _caller: LSPluginCaller
   ) {
@@ -516,7 +520,7 @@ export class LSPluginUser
   }
 
   // Life related
-  async ready(model?: any, callback?: any) {
+  async ready (model?: any, callback?: any) {
     if (this._connected) return
 
     try {
@@ -562,39 +566,39 @@ export class LSPluginUser
     }
   }
 
-  ensureConnected() {
+  ensureConnected () {
     if (!this._connected) {
       throw new Error('not connected')
     }
   }
 
-  beforeunload(callback: (e: any) => Promise<void>): void {
+  beforeunload (callback: (e: any) => Promise<void>): void {
     if (typeof callback !== 'function') return
     this._beforeunloadCallback = callback
   }
 
-  provideModel(model: Record<string, any>) {
+  provideModel (model: Record<string, any>) {
     this.caller._extendUserModel(model)
     return this
   }
 
-  provideTheme(theme: Theme) {
+  provideTheme (theme: Theme) {
     this.caller.call('provider:theme', theme)
     return this
   }
 
-  provideStyle(style: StyleString) {
+  provideStyle (style: StyleString) {
     this.caller.call('provider:style', style)
     return this
   }
 
-  provideUI(ui: UIOptions) {
+  provideUI (ui: UIOptions) {
     this.caller.call('provider:ui', ui)
     return this
   }
 
   // Settings related
-  useSettingsSchema(schema: Array<SettingSchemaDesc>) {
+  useSettingsSchema (schema: Array<SettingSchemaDesc>) {
     if (this.connected) {
       this.caller.call('settings:schema', {
         schema,
@@ -606,35 +610,35 @@ export class LSPluginUser
     return this
   }
 
-  updateSettings(attrs: Record<string, any>) {
+  updateSettings (attrs: Record<string, any>) {
     this.caller.call('settings:update', attrs)
     // TODO: update associated baseInfo settings
   }
 
-  onSettingsChanged<T = any>(cb: (a: T, b: T) => void): IUserOffHook {
+  onSettingsChanged<T = any> (cb: (a: T, b: T) => void): IUserOffHook {
     const type = 'settings:changed'
     this.on(type, cb)
     return () => this.off(type, cb)
   }
 
-  showSettingsUI() {
+  showSettingsUI () {
     this.caller.call('settings:visible:changed', { visible: true })
   }
 
-  hideSettingsUI() {
+  hideSettingsUI () {
     this.caller.call('settings:visible:changed', { visible: false })
   }
 
   // UI related
-  setMainUIAttrs(attrs: Partial<UIContainerAttrs>): void {
+  setMainUIAttrs (attrs: Partial<UIContainerAttrs>): void {
     this.caller.call('main-ui:attrs', attrs)
   }
 
-  setMainUIInlineStyle(style: CSS.Properties): void {
+  setMainUIInlineStyle (style: CSS.Properties): void {
     this.caller.call('main-ui:style', style)
   }
 
-  hideMainUI(opts?: { restoreEditingCursor: boolean }): void {
+  hideMainUI (opts?: { restoreEditingCursor: boolean }): void {
     const payload = {
       key: KEY_MAIN_UI,
       visible: false,
@@ -645,7 +649,7 @@ export class LSPluginUser
     this._ui.set(payload.key, payload)
   }
 
-  showMainUI(opts?: { autoFocus: boolean }): void {
+  showMainUI (opts?: { autoFocus: boolean }): void {
     const payload = {
       key: KEY_MAIN_UI,
       visible: true,
@@ -656,7 +660,7 @@ export class LSPluginUser
     this._ui.set(payload.key, payload)
   }
 
-  toggleMainUI(): void {
+  toggleMainUI (): void {
     const payload = { key: KEY_MAIN_UI, toggle: true }
     const state = this._ui.get(payload.key)
     if (state && state.visible) {
@@ -667,40 +671,40 @@ export class LSPluginUser
   }
 
   // Getters
-  get version(): string {
+  get version (): string {
     return this._version
   }
 
-  get isMainUIVisible(): boolean {
+  get isMainUIVisible (): boolean {
     const state = this._ui.get(KEY_MAIN_UI)
     return Boolean(state && state.visible)
   }
 
-  get connected(): boolean {
+  get connected (): boolean {
     return this._connected
   }
 
-  get baseInfo(): LSPluginBaseInfo {
+  get baseInfo (): LSPluginBaseInfo {
     return this._baseInfo
   }
 
-  get effect(): Boolean {
+  get effect (): Boolean {
     return checkEffect(this)
   }
 
-  get logger() {
+  get logger () {
     return logger
   }
 
-  get settings() {
+  get settings () {
     return this.baseInfo?.settings
   }
 
-  get caller(): LSPluginCaller {
+  get caller (): LSPluginCaller {
     return this._caller
   }
 
-  resolveResourceFullUrl(filePath: string) {
+  resolveResourceFullUrl (filePath: string) {
     this.ensureConnected()
     if (!filePath) return
     filePath = filePath.replace(/^[.\\/]+/, '')
@@ -710,12 +714,12 @@ export class LSPluginUser
   /**
    * @internal
    */
-  _makeUserProxy(target: any, tag?: UserProxyTags) {
+  _makeUserProxy (target: any, tag?: UserProxyTags) {
     const that = this
     const caller = this.caller
 
     return new Proxy(target, {
-      get(target: any, propKey, receiver) {
+      get (target: any, propKey, receiver) {
         const origMethod = target[propKey]
 
         return function (this: any, ...args: any) {
@@ -731,13 +735,23 @@ export class LSPluginUser
             if (hookMatcher != null) {
               const f = hookMatcher[0].toLowerCase()
               const s = hookMatcher.input!
-              const e = s.slice(f.length)
               const isOff = f === 'off'
               const pid = that.baseInfo.id
 
-              const type = `hook:${tag}:${safeSnakeCase(e)}`
-              const handler = args[0]
-              const opts = args[1]
+              let type = s.slice(f.length)
+              let handler = args[0]
+              let opts = args[1]
+
+              // condition mode
+              if (typeof handler === 'string' && typeof opts === 'function') {
+                handler = handler.replace(/^logseq./, ':')
+                type = `${type}${handler}`
+                handler = opts
+                opts = args[2]
+              }
+
+              type = `hook:${tag}:${safeSnakeCase(type)}`
+
               caller[f](type, handler)
 
               const unlisten = () => {
@@ -775,64 +789,64 @@ export class LSPluginUser
     })
   }
 
-  _execCallableAPIAsync(method: callableMethods, ...args) {
+  _execCallableAPIAsync (method: callableMethods, ...args) {
     return this._caller.callAsync(`api:call`, {
       method,
       args,
     })
   }
 
-  _execCallableAPI(method: callableMethods, ...args) {
+  _execCallableAPI (method: callableMethods, ...args) {
     this._caller.call(`api:call`, {
       method,
       args,
     })
   }
 
-  _callWin(...args) {
+  _callWin (...args) {
     return this._execCallableAPIAsync(`_callMainWin`, ...args)
   }
 
   /**
    * The interface methods of {@link IAppProxy}
    */
-  get App(): IAppProxy {
+  get App (): IAppProxy {
     return this._makeUserProxy(app, 'app')
   }
 
-  get Editor(): IEditorProxy {
+  get Editor (): IEditorProxy {
     return this._makeUserProxy(editor, 'editor')
   }
 
-  get DB(): IDBProxy {
+  get DB (): IDBProxy {
     return this._makeUserProxy(db, 'db')
   }
 
-  get Git(): IGitProxy {
+  get Git (): IGitProxy {
     return this._makeUserProxy(git, 'git')
   }
 
-  get UI(): IUIProxy {
+  get UI (): IUIProxy {
     return this._makeUserProxy(ui, 'ui')
   }
 
-  get Assets(): IAssetsProxy {
+  get Assets (): IAssetsProxy {
     return this._makeUserProxy(assets, 'assets')
   }
 
-  get FileStorage(): LSPluginFileStorage {
+  get FileStorage (): LSPluginFileStorage {
     let m = this._mFileStorage
     if (!m) m = this._mFileStorage = new LSPluginFileStorage(this)
     return m
   }
 
-  get Request(): LSPluginRequest {
+  get Request (): LSPluginRequest {
     let m = this._mRequest
     if (!m) m = this._mRequest = new LSPluginRequest(this)
     return m
   }
 
-  get Experiments(): LSPluginExperiments {
+  get Experiments (): LSPluginExperiments {
     let m = this._mExperiments
     if (!m) m = this._mExperiments = new LSPluginExperiments(this)
     return m
@@ -844,7 +858,7 @@ export * from './LSPlugin'
 /**
  * @internal
  */
-export function setupPluginUserInstance(
+export function setupPluginUserInstance (
   pluginBaseInfo: LSPluginBaseInfo,
   pluginCaller: LSPluginCaller
 ) {

+ 42 - 37
libs/src/helpers.ts

@@ -67,7 +67,7 @@ export class PluginLogger extends EventEmitter<'change'> {
   }
 
   write(type: string, payload: any[], inConsole?: boolean) {
-    if (payload?.length && (true === payload[payload.length - 1])) {
+    if (payload?.length && true === payload[payload.length - 1]) {
       inConsole = true
       payload.pop()
     }
@@ -117,9 +117,13 @@ export class PluginLogger extends EventEmitter<'change'> {
 }
 
 export function isValidUUID(s: string) {
-  return (typeof s === 'string' &&
-    (s.length === 36) &&
-    (/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi).test(s))
+  return (
+    typeof s === 'string' &&
+    s.length === 36 &&
+    /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi.test(
+      s
+    )
+  )
 }
 
 export function genID() {
@@ -259,9 +263,9 @@ export function setupInjectedStyle(
   el.textContent = style
 
   attrs &&
-  Object.entries(attrs).forEach(([k, v]) => {
-    el.setAttribute(k, v)
-  })
+    Object.entries(attrs).forEach(([k, v]) => {
+      el.setAttribute(k, v)
+    })
 
   document.head.append(el)
 
@@ -337,22 +341,22 @@ export function setupInjectedUI(
 
     // update attributes
     attrs &&
-    Object.entries(attrs).forEach(([k, v]) => {
-      el.setAttribute(k, v)
-    })
+      Object.entries(attrs).forEach(([k, v]) => {
+        el.setAttribute(k, v)
+      })
 
     let positionDirty = el.dataset.dx != null
     ui.style &&
-    Object.entries(ui.style).forEach(([k, v]) => {
-      if (
-        positionDirty &&
-        ['left', 'top', 'bottom', 'right', 'width', 'height'].includes(k)
-      ) {
-        return
-      }
-
-      el.style[k] = v
-    })
+      Object.entries(ui.style).forEach(([k, v]) => {
+        if (
+          positionDirty &&
+          ['left', 'top', 'bottom', 'right', 'width', 'height'].includes(k)
+        ) {
+          return
+        }
+
+        el.style[k] = v
+      })
     return
   }
 
@@ -372,14 +376,14 @@ export function setupInjectedUI(
   content.innerHTML = ui.template
 
   attrs &&
-  Object.entries(attrs).forEach(([k, v]) => {
-    el.setAttribute(k, v)
-  })
+    Object.entries(attrs).forEach(([k, v]) => {
+      el.setAttribute(k, v)
+    })
 
   ui.style &&
-  Object.entries(ui.style).forEach(([k, v]) => {
-    el.style[k] = v
-  })
+    Object.entries(ui.style).forEach(([k, v]) => {
+      el.style[k] = v
+    })
 
   let teardownUI: () => void
   let disposeFloat: () => void
@@ -392,11 +396,11 @@ export function setupInjectedUI(
     el.classList.add('lsp-ui-float-container', 'visible')
     disposeFloat =
       (pl._setupResizableContainer(el, key),
-        pl._setupDraggableContainer(el, {
-          key,
-          close: () => teardownUI(),
-          title: attrs?.title,
-        }))
+      pl._setupDraggableContainer(el, {
+        key,
+        close: () => teardownUI(),
+        title: attrs?.title,
+      }))
   }
 
   if (!!slot && ui.reset) {
@@ -424,7 +428,7 @@ export function setupInjectedUI(
     'keydown',
     'change',
     'input',
-    'contextmenu'
+    'contextmenu',
   ].forEach((type) => {
     el.addEventListener(
       type,
@@ -435,7 +439,8 @@ export function setupInjectedUI(
 
         const { preventDefault } = trigger.dataset
         const msgType = trigger.dataset[`on${ucFirst(type)}`]
-        if (msgType) pl.caller?.callUserModel(msgType, transformableEvent(trigger, e))
+        if (msgType)
+          pl.caller?.callUserModel(msgType, transformableEvent(trigger, e))
         if (preventDefault?.toLowerCase() === 'true') e.preventDefault()
       },
       false
@@ -455,12 +460,12 @@ export function setupInjectedUI(
   return teardownUI
 }
 
-export function cleanInjectedUI(
-  id: string
-) {
+export function cleanInjectedUI(id: string) {
   if (!injectedUIEffects.has(id)) return
   const clean = injectedUIEffects.get(id)
-  try { clean() } catch (e) {
+  try {
+    clean()
+  } catch (e) {
     console.warn('[CLEAN Injected UI] ', id, e)
   }
 }

+ 2 - 1
libs/src/modules/LSPlugin.Experiments.ts

@@ -76,7 +76,8 @@ export class LSPluginExperiments {
 
     return host.logseq.api.exper_register_extensions_enhancer(
       this.ctx.baseInfo.id,
-      type, enhancer
+      type,
+      enhancer
     )
   }
 

+ 34 - 36
libs/src/modules/LSPlugin.Request.ts

@@ -31,23 +31,19 @@ export class LSPluginRequestTask<R = any> {
     private _requestId: RequestTaskID,
     private _requestOptions: Partial<IRequestOptions> = {}
   ) {
-
     this._promise = new Promise<any>((resolve, reject) => {
       if (!this._requestId) {
         return reject(null)
       }
 
       // task result listener
-      this._client.once(
-        genTaskCallbackType(this._requestId),
-        (e) => {
-          if (e && e instanceof Error) {
-            reject(e)
-          } else {
-            resolve(e)
-          }
+      this._client.once(genTaskCallbackType(this._requestId), (e) => {
+        if (e && e instanceof Error) {
+          reject(e)
+        } else {
+          resolve(e)
         }
-      )
+      })
     })
 
     const { success, fail, final } = this._requestOptions
@@ -65,15 +61,9 @@ export class LSPluginRequestTask<R = any> {
   }
 
   abort() {
-    if (
-      !this._requestOptions.abortable ||
-      this._aborted
-    ) return
-
-    this._client.ctx._execCallableAPI(
-      'http_request_abort',
-      this._requestId
-    )
+    if (!this._requestOptions.abortable || this._aborted) return
+
+    this._client.ctx._execCallableAPI('http_request_abort', this._requestId)
 
     this._aborted = true
   }
@@ -99,15 +89,12 @@ export class LSPluginRequest extends EventEmitter {
     super()
 
     // request callback listener
-    this.ctx.caller.on(
-      CLIENT_MSG_CALLBACK,
-      (e: any) => {
-        const reqId = e?.requestId
-        if (!reqId) return
+    this.ctx.caller.on(CLIENT_MSG_CALLBACK, (e: any) => {
+      const reqId = e?.requestId
+      if (!reqId) return
 
-        this.emit(genTaskCallbackType(reqId), e?.payload)
-      }
-    )
+      this.emit(genTaskCallbackType(reqId), e?.payload)
+    })
   }
 
   static createRequestTask(
@@ -115,21 +102,32 @@ export class LSPluginRequest extends EventEmitter {
     requestID: RequestTaskID,
     requestOptions: Partial<IRequestOptions>
   ) {
-    return new LSPluginRequestTask(
-      client, requestID, requestOptions
-    )
+    return new LSPluginRequestTask(client, requestID, requestOptions)
   }
 
-  async _request<R extends {},
-    T extends WithOptional<IRequestOptions<R>, keyof Omit<IRequestOptions, 'url'>>>(options: T):
-    Promise<T extends Pick<IRequestOptions, 'abortable'> ? LSPluginRequestTask<R> : R> {
+  async _request<
+    R extends {},
+    T extends WithOptional<
+      IRequestOptions<R>,
+      keyof Omit<IRequestOptions, 'url'>
+    >
+  >(
+    options: T
+  ): Promise<
+    T extends Pick<IRequestOptions, 'abortable'> ? LSPluginRequestTask<R> : R
+  > {
     const pid = this.ctx.baseInfo.id
     const { success, fail, final, ...requestOptions } = options
-    const reqID = this.ctx.Experiments.invokeExperMethod('request', pid, requestOptions)
+    const reqID = this.ctx.Experiments.invokeExperMethod(
+      'request',
+      pid,
+      requestOptions
+    )
 
     const task = LSPluginRequest.createRequestTask(
       this.ctx.Request,
-      reqID, options
+      reqID,
+      options
     )
 
     if (!requestOptions.abortable) {
@@ -142,4 +140,4 @@ export class LSPluginRequest extends EventEmitter {
   get ctx(): LSPluginUser {
     return this._ctx
   }
-}
+}

+ 48 - 51
libs/src/modules/LSPlugin.Search.ts

@@ -3,7 +3,6 @@ import { LSPluginUser } from '../LSPlugin.user'
 import { isArray, isFunction, mapKeys } from 'lodash-es'
 
 export class LSPluginSearchService {
-
   /**
    * @param ctx
    * @param serviceHooks
@@ -22,61 +21,59 @@ export class LSPluginSearchService {
     // hook events TODO: remove listeners
     const wrapHookEvent = (k) => `service:search:${k}:${serviceHooks.name}`
 
-    Object.entries(
-      {
-        query: {
-          f: 'onQuery', args: ['graph', 'q', true], reply: true,
-          transformOutput: (data: any) => {
-            // TODO: transform keys?
-            if (isArray(data?.blocks)) {
-              data.blocks = data.blocks.map(it => {
-                return it && mapKeys(it, (_, k) => `block/${k}`)
-              })
-            }
-
-            return data
+    Object.entries({
+      query: {
+        f: 'onQuery',
+        args: ['graph', 'q', true],
+        reply: true,
+        transformOutput: (data: any) => {
+          // TODO: transform keys?
+          if (isArray(data?.blocks)) {
+            data.blocks = data.blocks.map((it) => {
+              return it && mapKeys(it, (_, k) => `block/${k}`)
+            })
           }
+
+          return data
         },
-        rebuildBlocksIndice: { f: 'onIndiceInit', args: ['graph', 'blocks'] },
-        transactBlocks: { f: 'onBlocksChanged', args: ['graph', 'data'] },
-        truncateBlocks: { f: 'onIndiceReset', args: ['graph'] },
-        removeDb: { f: 'onGraph', args: ['graph'] }
-      }
-    ).forEach(
-      ([k, v]) => {
-        const hookEvent = wrapHookEvent(k)
-        ctx.caller.on(hookEvent, async (payload: any) => {
-          if (isFunction(serviceHooks?.[v.f])) {
-            let ret = null
+      },
+      rebuildBlocksIndice: { f: 'onIndiceInit', args: ['graph', 'blocks'] },
+      transactBlocks: { f: 'onBlocksChanged', args: ['graph', 'data'] },
+      truncateBlocks: { f: 'onIndiceReset', args: ['graph'] },
+      removeDb: { f: 'onGraph', args: ['graph'] },
+    }).forEach(([k, v]) => {
+      const hookEvent = wrapHookEvent(k)
+      ctx.caller.on(hookEvent, async (payload: any) => {
+        if (isFunction(serviceHooks?.[v.f])) {
+          let ret = null
 
-            try {
-              ret = await serviceHooks[v.f].apply(
-                serviceHooks, (v.args || []).map((prop: any) => {
-                  if (!payload) return
-                  if (prop === true) return payload
-                  if (payload.hasOwnProperty(prop)) {
-                    const ret = payload[prop]
-                    delete payload[prop]
-                    return ret
-                  }
-                })
-              )
+          try {
+            ret = await serviceHooks[v.f].apply(
+              serviceHooks,
+              (v.args || []).map((prop: any) => {
+                if (!payload) return
+                if (prop === true) return payload
+                if (payload.hasOwnProperty(prop)) {
+                  const ret = payload[prop]
+                  delete payload[prop]
+                  return ret
+                }
+              })
+            )
 
-              if (v.transformOutput) {
-                ret = v.transformOutput(ret)
-              }
-            } catch (e) {
-              console.error('[SearchService] ', e)
-              ret = e
-            } finally {
-              if (v.reply) {
-                ctx.caller.call(
-                  `${hookEvent}:reply`, ret
-                )
-              }
+            if (v.transformOutput) {
+              ret = v.transformOutput(ret)
+            }
+          } catch (e) {
+            console.error('[SearchService] ', e)
+            ret = e
+          } finally {
+            if (v.reply) {
+              ctx.caller.call(`${hookEvent}:reply`, ret)
             }
           }
-        })
+        }
       })
+    })
   }
-}
+}

+ 1 - 1
libs/src/modules/LSPlugin.Storage.ts

@@ -73,7 +73,7 @@ class LSPluginFileStorage implements IAsyncStorage {
   allKeys(): Promise<Array<string>> {
     return this.ctx.caller.callAsync(`api:call`, {
       method: 'list-plugin-storage-files',
-      args: [this.ctxId, this.opts?.assets]
+      args: [this.ctxId, this.opts?.assets],
     })
   }
 

+ 3 - 1
libs/src/postmate/index.ts

@@ -81,7 +81,9 @@ export const sanitize = (message, allowedOrigin) => {
  */
 export const resolveValue = (model, property, args) => {
   const unwrappedContext =
-    typeof model[property] === 'function' ? model[property].apply(null, args) : model[property]
+    typeof model[property] === 'function'
+      ? model[property].apply(null, args)
+      : model[property]
   return Promise.resolve(unwrappedContext)
 }
 

Plik diff jest za duży
+ 0 - 0
resources/js/lsplugin.core.js


+ 2 - 2
src/main/frontend/components/command_palette.cljs

@@ -1,7 +1,7 @@
 (ns frontend.components.command-palette
   (:require [frontend.handler.command-palette :as cp]
             [frontend.modules.shortcut.core :as shortcut]
-            [frontend.modules.shortcut.data-helper :as shortcut-helper]
+            [frontend.modules.shortcut.utils :as shortcut-utils]
             [frontend.context.i18n :refer [t]]
             [frontend.search :as search]
             [frontend.ui :as ui]
@@ -11,7 +11,7 @@
 
 (defn translate [t {:keys [id desc]}]
   (when id
-    (let [desc-i18n (t (shortcut-helper/decorate-namespace id))]
+    (let [desc-i18n (t (shortcut-utils/decorate-namespace id))]
       (if (string/starts-with? desc-i18n "{Missing key")
         desc
         desc-i18n))))

+ 2 - 2
src/main/frontend/components/plugins.css

@@ -609,7 +609,7 @@
 
     .cp__settings-inner {
       aside {
-        @apply max-h-[70vh] overflow-auto mb-[-17px] p-3;
+        @apply max-h-[70vh] overflow-auto p-3;
 
         ul {
           @apply list-none p-0 m-0;
@@ -991,7 +991,7 @@ html[data-theme='dark'] {
 .ui__modal[label=plugins-dashboard] {
   .panel-content {
     overflow-y: auto;
-    max-height: calc(100vh - 100px);
+    max-height: calc(100vh - 50px);
   }
 }
 

+ 1 - 1
src/main/frontend/components/plugins_settings.cljs

@@ -29,7 +29,7 @@
   [val {:keys [key type title default description inputAs]} update-setting!]
 
   [:div.desc-item.as-input
-   {:data-key key}
+   {:data-key key :key key}
    [:h2 [:code key] (ui/icon "caret-right") [:strong title]]
 
    [:label.form-control

+ 32 - 5
src/main/frontend/components/settings.cljs

@@ -23,6 +23,7 @@
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.instrumentation.core :as instrument]
             [frontend.modules.shortcut.data-helper :as shortcut-helper]
+            [frontend.components.shortcut2 :as shortcut2]
             [frontend.spec.storage :as storage-spec]
             [frontend.state :as state]
             [frontend.storage :as storage]
@@ -578,13 +579,13 @@
 (rum/defc user-proxy-settings
   [{:keys [type protocol host port] :as agent-opts}]
   (ui/button [:span.flex.items-center
-              [:strong.pr-1
+              [:span.pr-1
                (case type
                  "system" "System Default"
                  "direct" "Direct"
                  (and protocol host port (str protocol "://" host ":" port)))]
               (ui/icon "edit")]
-             :small? true
+             :class "text-sm p-1"
              :on-click #(state/set-sub-modal!
                          (fn [_] (plugins/user-proxy-settings-panel agent-opts))
                          {:id :https-proxy-panel :center? true})))
@@ -1037,18 +1038,41 @@
 
 (def DEFAULT-ACTIVE-TAB-STATE (if config/ENABLE-SETTINGS-ACCOUNT-TAB [:account :account] [:general :general]))
 
+(rum/defc settings-effect
+  < rum/static
+  [active]
+
+  (rum/use-effect!
+    (fn []
+      (let [active (and (sequential? active) (name (first active)))
+            ^js ds (.-dataset js/document.body)]
+        (if active
+          (set! (.-settingsTab ds) active)
+          (js-delete ds "settingsTab"))
+        #(js-delete ds "settingsTab")))
+    [active])
+
+  [:<>])
+
 (rum/defcs settings
   < (rum/local DEFAULT-ACTIVE-TAB-STATE ::active)
     {:will-mount
      (fn [state]
        (state/load-app-user-cfgs)
        state)
+     :did-mount
+     (fn [state]
+       (let [active-tab (first (:rum/args state))
+             *active (::active state)]
+         (when (keyword? active-tab)
+           (reset! *active [active-tab nil])))
+       state)
      :will-unmount
      (fn [state]
        (state/close-settings!)
        state)}
     rum/reactive
-  [state]
+  [state _active-tab]
   (let [current-repo (state/sub :git/current-repo)
         ;; enable-block-timestamps? (state/enable-block-timestamps?)
         _installed-plugins (state/sub :plugin/installed-plugins)
@@ -1056,9 +1080,8 @@
         *active (::active state)]
 
     [:div#settings.cp__settings-main
-
+     (settings-effect @*active)
      [:div.cp__settings-inner
-
       [:aside.md:w-64 {:style {:min-width "10rem"}}
        [:header.cp__settings-header
         (ui/icon "settings")
@@ -1069,6 +1092,7 @@
                 [:account "account" (t :settings-page/tab-account) (ui/icon "user-circle")])
                [:general "general" (t :settings-page/tab-general) (ui/icon "adjustments")]
                [:editor "editor" (t :settings-page/tab-editor) (ui/icon "writing")]
+               [:keymap "keymap" (t :settings-page/tab-keymap) (ui/icon "keyboard")]
 
                (when (util/electron?)
                  [:version-control "git" (t :settings-page/tab-version-control) (ui/icon "history")])
@@ -1114,6 +1138,9 @@
          :editor
          (settings-editor current-repo)
 
+         :keymap
+         (shortcut2/shortcut-keymap-x)
+
          :version-control
          (settings-git)
 

+ 40 - 52
src/main/frontend/components/settings.css

@@ -1,23 +1,35 @@
 .cp__settings {
-  &-main {
-    aside {
+  &-inner {
+    @apply flex flex-col md:flex-row;
+
+    > aside {
       @apply bg-gray-400/5 p-4;
+
+      > ul > li {
+        > a {
+          @apply mb-2;
+
+          > strong {
+            font-size: 14px;
+            font-weight: normal;
+            padding-left: 5px;
+            opacity: .9;
+          }
+        }
+
+        &.active {
+          background-color: var(--ls-quaternary-background-color);
+        }
+      }
     }
 
-    article {
+    > article {
       @apply p-4 flex-1 min-h-[12rem] w-auto overflow-y-auto;
       @apply md:max-h-[70vh] md:w-[40rem];
-      /* margin-right: -17px; */
-      /* margin-bottom: -17px; */
-
-      @screen md {
-        /* max-height: 70vh; */
-        /* width: 680px; */
-      }
     }
 
-    aside > .cp__settings-header,
-    article > .cp__settings-header {
+    > aside > .cp__settings-header,
+    > article > .cp__settings-header {
       @apply h-10 py-2 flex flex-row items-center justify-start gap-2;
     }
 
@@ -41,13 +53,13 @@
       @apply text-xl lowercase;
     }
 
-    h1.cp__settings-modal-title:first-letter, 
+    h1.cp__settings-modal-title:first-letter,
     h1.cp__settings-category-title:first-letter {
       @apply uppercase;
     }
 
     .settings-menu {
-      @apply p-0 m-0 mt-4 pr-3; 
+      @apply p-0 m-0 mt-4;
     }
 
     .settings-menu-item {
@@ -56,46 +68,10 @@
     }
 
     .settings-menu-link {
-      @apply px-2 py-1.5 select-none; 
+      @apply px-2 py-1.5 select-none;
       color: var(--ls-primary-text-color);
     }
-  }
-
-  &-inner {
-    @apply flex flex-col md:flex-row;
-
-    > aside {
-
-      ul {
 
-        > li {
-
-          > a {
-
-            > i {
-              overflow: hidden;
-              opacity: .9;
-            }
-
-            > strong {
-              font-size: 14px;
-              font-weight: normal;
-              padding-left: 5px;
-              margin-top: 2px;
-              opacity: .9;
-            }
-          }
-
-          &.active {
-            background-color: var(--ls-quaternary-background-color);
-
-            i {
-              opacity: 1;
-            }
-          }
-        }
-      }
-    }
 
     &.no-aside {
       > article {
@@ -392,7 +368,7 @@
         z-index: 1;
         width: 100px;
         max-height: 180px;
-        border:1px solid var(--ls-border-color);
+        border: 1px solid var(--ls-border-color);
         border-radius: 4px;
         overflow: auto;
         overflow: overlay;
@@ -465,3 +441,15 @@ svg.git {
 svg.cmd {
   margin-left: -1px;
 }
+
+body[data-settings-tab=keymap] {
+  .cp__settings-inner {
+    > article {
+      @apply md:w-[70vw] xl:max-w-[850px] p-0;
+
+      > header {
+        @apply p-4 pb-2 h-auto;
+      }
+    }
+  }
+}

+ 4 - 15
src/main/frontend/components/shortcut.cljs

@@ -3,6 +3,7 @@
             [frontend.context.i18n :refer [t]]
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.modules.shortcut.data-helper :as dh]
+            [frontend.modules.shortcut.utils :as shortcut-utils]
             [frontend.state :as state]
             [frontend.ui :as ui]
             [frontend.extensions.latex :as latex]
@@ -104,7 +105,7 @@
                                [:code.text-xs (namespace k)]
                                [:small.pl-1 (:desc cmd)]]
 
-                              (not plugin?) (-> k (dh/decorate-namespace) (t))
+                              (not plugin?) (-> k (shortcut-utils/decorate-namespace) (t))
                               :else (str k))]
                   [:tr {:key (str k)}
                    [:td.text-left.flex.items-center label]
@@ -204,23 +205,11 @@
    (shortcut-table :shortcut.category/block-selection true)
    (shortcut-table :shortcut.category/formatting true)
    (shortcut-table :shortcut.category/toggle true)
-   (when (state/enable-whiteboards?) (shortcut-table :shortcut.category/whiteboard true))
+   (when (state/enable-whiteboards?)
+     (shortcut-table :shortcut.category/whiteboard true))
    (shortcut-table :shortcut.category/plugins true)
    (shortcut-table :shortcut.category/others true)])
 
-(rum/defc keymap-pane
-  []
-  (let [[ready?, set-ready!] (rum/use-state false)]
-    (rum/use-effect!
-      (fn [] (js/setTimeout #(set-ready! true) 32))
-      [])
-
-    [:div.cp__keymap-pane
-     [:h1.pb-2.text-3xl.pt-2 "Keymap"]
-     (if ready?
-       (keymap-tables)
-       [:p.flex.justify-center.py-20 (ui/loading "")])]))
-
 (rum/defc shortcut-page
   [{:keys [show-title?]
     :or {show-title? true}}]

+ 165 - 0
src/main/frontend/components/shortcut.css

@@ -25,4 +25,169 @@
       }
     }
   }
+}
+
+.cp__shortcut-page-x {
+  @apply relative;
+
+  &-pane-controls {
+    @apply flex space-x-3 absolute top-[-4px] right-4 items-center;
+
+    .search-input-wrap {
+      @apply pr-1 relative;
+
+      a.x {
+        @apply flex items-center absolute right-1 top-0 py-[7px] px-1 opacity-60
+        hover:opacity-90;
+      }
+    }
+
+    input.form-input {
+      @apply py-1;
+    }
+
+    a.icon-link {
+      @apply opacity-80 hover:opacity-100 active:opacity-40 select-none;
+
+      color: var(--ls-secondary-text-color);
+    }
+
+    .keyboard-filter {
+      .dropdown-wrapper {
+        @apply shadow-lg w-[18rem];
+      }
+
+      .keyboard-filter-record {
+        > h2 {
+          @apply flex items-center justify-between px-1.5 py-1;
+
+          background-color: var(--ls-secondary-background-color);
+
+          > strong {
+            @apply text-[12px] opacity-80;
+
+            font-weight: 400;
+          }
+        }
+      }
+    }
+  }
+
+  > header {
+    @apply px-4 pb-4 pt-2;
+
+    > h2 {
+      @apply relative top-[-6px];
+    }
+  }
+
+  > article {
+    @apply relative pb-4 overflow-y-auto;
+
+    max-height: calc(70vh - 100px);
+    overflow-y: overlay;
+
+    > ul {
+      @apply px-4 m-0 py-0;
+
+      li {
+        @apply text-[15px] px-1;
+
+        &.th {
+          @apply rounded mb-2 sticky top-0 cursor-pointer
+          select-none active:opacity-80 px-2 py-1 z-[1];
+
+          background-color: var(--ls-tertiary-background-color);
+        }
+
+        .label-wrap {
+          @apply flex flex-1;
+        }
+
+        .action-wrap {
+          @apply flex space-x-2 items-center flex-nowrap
+          select-none active:opacity-70;
+
+          &.disabled {
+            @apply opacity-60 cursor-default;
+          }
+        }
+      }
+    }
+  }
+
+  &-record-dialog-inner {
+    @apply py-[28px] m-[-30px] px-[20px];
+
+    h1 {
+      @apply relative top-[-8px];
+    }
+
+    &:active, &:focus, &:focus-within {
+      outline: burlywood hidden medium;
+    }
+
+    .shortcuts-keys-wrap {
+      @apply flex items-center my-4 flex-wrap;
+
+      .shortcut-record-control {
+        @apply flex space-x-1 items-center select-none
+        rounded border-[2px] py-[2px] px-[2px];
+      }
+
+      .keyboard-shortcut {
+        > code {
+          @apply relative select-none tracking-wider;
+
+          a.x {
+            @apply hidden absolute right-[-8px] top-[-6px] h-[16px] w-[16px]
+            rounded-full bg-red-500 text-white leading-none items-center
+            justify-center cursor-pointer opacity-80 hover:opacity-100 active:opacity-50;
+          }
+
+          &:hover a.x {
+            @apply flex;
+          }
+        }
+      }
+    }
+
+    &.keypressed {
+      .shortcut-record-control {
+        @apply pt-0
+      }
+    }
+
+    .action-btns {
+      .keyboard-shortcut code {
+        @apply rounded-[3px];
+      }
+    }
+
+    .reset-btn {
+      @apply ml-4 opacity-50 cursor-default;
+    }
+
+    &.dirty {
+      .reset-btn {
+        @apply opacity-100 cursor-pointer;
+      }
+    }
+  }
+}
+
+.cp__shortcut-conflicts-list {
+  &-wrap {
+    > section {
+      @apply bg-gray-3 border-[2px] mb-3 dark:bg-transparent;
+
+      > ul {
+        @apply px-2 pb-2 m-0 list-none;
+      }
+
+      > h2 {
+        @apply flex items-center p-2 text-red-9 text-sm space-x-1 font-extrabold;
+      }
+    }
+  }
 }

+ 476 - 0
src/main/frontend/components/shortcut2.cljs

@@ -0,0 +1,476 @@
+(ns frontend.components.shortcut2
+  (:require [clojure.string :as string]
+            [rum.core :as rum]
+            [frontend.context.i18n :refer [t]]
+            [cljs-bean.core :as bean]
+            [frontend.state :as state]
+            [frontend.search :as search]
+            [frontend.ui :as ui]
+            [frontend.rum :as r]
+            [goog.events :as events]
+            [promesa.core :as p]
+            [frontend.handler.notification :as notification]
+            [frontend.modules.shortcut.core :as shortcut]
+            [frontend.modules.shortcut.data-helper :as dh]
+            [frontend.util :as util]
+            [frontend.modules.shortcut.utils :as shortcut-utils]
+            [frontend.modules.shortcut.config :as shortcut-config])
+  (:import [goog.events KeyHandler]))
+
+(defonce categories
+         (vector :shortcut.category/basics
+                 :shortcut.category/navigating
+                 :shortcut.category/block-editing
+                 :shortcut.category/block-command-editing
+                 :shortcut.category/block-selection
+                 :shortcut.category/formatting
+                 :shortcut.category/toggle
+                 :shortcut.category/whiteboard
+                 :shortcut.category/plugins
+                 :shortcut.category/others))
+
+(defonce *refresh-sentry (atom 0))
+(defn refresh-shortcuts-list! [] (reset! *refresh-sentry (inc @*refresh-sentry)))
+(defonce *global-listener-setup? (atom false))
+(defonce *customize-modal-life-sentry (atom 0))
+
+(defn- to-vector [v]
+  (when-not (nil? v)
+    (if (sequential? v) (vec v) [v])))
+
+(declare customize-shortcut-dialog-inner)
+
+(rum/defc keyboard-filter-record-inner
+  [keystroke set-keystroke! close-fn]
+
+  (let [keypressed? (not= "" keystroke)]
+
+    (rum/use-effect!
+      (fn []
+        (let [key-handler (KeyHandler. js/document)]
+          ;; setup
+          (util/profile
+            "[shortcuts] unlisten*"
+            (shortcut/unlisten-all! true))
+          (events/listen key-handler "key"
+                         (fn [^js e]
+                           (.preventDefault e)
+                           (set-keystroke! #(util/trim-safe (str % (shortcut/keyname e))))))
+
+          ;; teardown
+          #(do
+             (util/profile
+               "[shortcuts] listen*"
+               (shortcut/listen-all!))
+             (.dispose key-handler))))
+      [])
+
+    [:div.keyboard-filter-record
+     [:h2
+      [:strong (t :keymap/keystroke-filter)]
+      [:span.flex.space-x-2
+       (when keypressed?
+         [:a.flex.items-center
+          {:on-click #(set-keystroke! "")} (ui/icon "zoom-reset" {:size 12})])
+       [:a.flex.items-center
+        {:on-click #(do (close-fn) (set-keystroke! ""))} (ui/icon "x" {:size 12})]]]
+     [:div.wrap.p-2
+      (if-not keypressed?
+        [:small (t :keymap/keystroke-record-desc)]
+        (when-not (string/blank? keystroke)
+          (ui/render-keyboard-shortcut [keystroke])))]]))
+
+(rum/defc pane-controls
+  [q set-q! filters set-filters! keystroke set-keystroke! toggle-categories-fn]
+  (let [*search-ref (rum/use-ref nil)]
+    [:div.cp__shortcut-page-x-pane-controls
+     [:a.flex.items-center.icon-link
+      {:on-click toggle-categories-fn
+       :title "Toggle categories pane"}
+      (ui/icon "fold")]
+
+     [:a.flex.items-center.icon-link
+      {:on-click refresh-shortcuts-list!
+       :title "Refresh all"}
+      (ui/icon "refresh")]
+
+     [:span.search-input-wrap
+      [:input.form-input.is-small
+       {:placeholder (t :keymap/search)
+        :ref         *search-ref
+        :value       (or q "")
+        :auto-focus  true
+        :on-key-down #(when (= 27 (.-keyCode %))
+                        (util/stop %)
+                        (if (string/blank? q)
+                          (some-> (rum/deref *search-ref) (.blur))
+                          (set-q! "")))
+        :on-change   #(let [v (util/evalue %)]
+                        (set-q! v))}]
+
+      (when-not (string/blank? q)
+        [:a.x
+         {:on-click (fn []
+                      (set-q! "")
+                      (js/setTimeout #(some-> (rum/deref *search-ref) (.focus)) 50))}
+         (ui/icon "x" {:size 14})])]
+
+     ;; keyboard filter
+     (ui/dropdown
+       (fn [{:keys [toggle-fn]}]
+         [:a.flex.items-center.icon-link
+          {:on-click toggle-fn} (ui/icon "keyboard")
+
+          (when-not (string/blank? keystroke)
+            (ui/point "bg-red-600.absolute" 4 {:style {:right -2 :top -2}}))])
+       (fn [{:keys [close-fn]}]
+         (keyboard-filter-record-inner keystroke set-keystroke! close-fn))
+       {:outside?      true
+        :trigger-class "keyboard-filter"})
+
+     ;; other filter
+     (ui/dropdown-with-links
+       (fn [{:keys [toggle-fn]}]
+         [:a.flex.items-center.icon-link.relative
+          {:on-click toggle-fn}
+          (ui/icon "filter")
+
+          (when (seq filters)
+            (ui/point "bg-red-600.absolute" 4 {:style {:right -2 :top -2}}))])
+
+       (for [k [:All :Disabled :Unset :Custom]
+             :let [all? (= k :All)
+                   checked? (or (contains? filters k) (and all? (nil? (seq filters))))]]
+
+         {:title   (if all? (t :keymap/all) (t (keyword :keymap (string/lower-case (name k)))))
+          :icon    (ui/icon (if checked? "checkbox" "square"))
+          :options {:on-click #(set-filters! (if all? #{} (let [f (if checked? disj conj)] (f filters k))))}})
+
+       nil)]))
+
+(rum/defc shortcut-desc-label
+  [id binding-map]
+  (when-let [id' (and id binding-map (some-> (str id) (string/replace "plugin." "")))]
+    [:span {:title (str id' "#" (some-> (:handler-id binding-map) (name)))}
+     [:span.pl-1 (dh/get-shortcut-desc (assoc binding-map :id id))]
+     [:small.pl-1 [:code.text-xs (str id')]]]))
+
+(defn- open-customize-shortcut-dialog!
+  [id]
+  (when-let [{:keys [binding user-binding] :as m} (dh/shortcut-item id)]
+    (let [binding (to-vector binding)
+          user-binding (and user-binding (to-vector user-binding))
+          modal-id (str :customize-shortcut id)
+          label (shortcut-desc-label id m)
+          args [id label binding user-binding
+                {:saved-cb (fn [] (-> (p/delay 500) (p/then refresh-shortcuts-list!)))
+                 :modal-id modal-id}]]
+      (state/set-sub-modal!
+        (fn [] (apply customize-shortcut-dialog-inner args))
+        {:center? true
+         :id      modal-id
+         :payload args}))))
+
+(rum/defc shortcut-conflicts-display
+  [_k conflicts-map]
+
+  [:div.cp__shortcut-conflicts-list-wrap
+   (for [[g ks] conflicts-map]
+     [:section.relative
+      [:h2 (ui/icon "alert-triangle" {:size 15})
+       [:span (t :keymap/conflicts-for-label)]
+       [:code (shortcut-utils/decorate-binding g)]]
+      [:ul
+       (for [v (vals ks)
+             :let [k (first v)
+                   vs (second v)]]
+         (for [[id' handler-id] vs
+               :let [m (dh/shortcut-item id')]
+               :when (not (nil? m))]
+           [:li
+            {:key (str id')}
+            [:a.select-none.hover:underline
+             {:on-click #(open-customize-shortcut-dialog! id')
+              :title (str handler-id)}
+             [:code.inline-block.mr-1.text-xs
+              (shortcut-utils/decorate-binding k)]
+             [:span
+              (dh/get-shortcut-desc m)
+              (ui/icon "external-link" {:size 18})]
+             [:code [:small (str id')]]]]))]])])
+
+(rum/defc ^:large-vars/cleanup-todo customize-shortcut-dialog-inner
+  [k action-name binding user-binding {:keys [saved-cb modal-id]}]
+  (let [*ref-el (rum/use-ref nil)
+        [modal-life _] (r/use-atom *customize-modal-life-sentry)
+        [keystroke set-keystroke!] (rum/use-state "")
+        [current-binding set-current-binding!] (rum/use-state (or user-binding binding))
+        [key-conflicts set-key-conflicts!] (rum/use-state nil)
+
+        handler-id (rum/use-memo #(dh/get-group k))
+        dirty? (not= (or user-binding binding) current-binding)
+        keypressed? (not= "" keystroke)
+
+        save-keystroke-fn!
+        (fn []
+          ;; parse current binding conflicts
+          (if-let [current-conflicts (seq (dh/parse-conflicts-from-binding current-binding keystroke))]
+            (notification/show!
+              (str "Shortcut conflicts from existing binding: "
+                   (pr-str (some->> current-conflicts (map #(shortcut-utils/decorate-binding %)))))
+              :error true :shortcut-conflicts/warning 5000)
+
+            ;; get conflicts from the existed bindings map
+            (let [conflicts-map (dh/get-conflicts-by-keys keystroke handler-id)]
+              (if-not (seq conflicts-map)
+                (do (set-current-binding! (conj current-binding keystroke))
+                    (set-keystroke! "")
+                    (set-key-conflicts! nil))
+
+                ;; show conflicts
+                (set-key-conflicts! conflicts-map)))))]
+
+    (rum/use-effect!
+      (fn []
+        (let [mid (state/sub :modal/id)
+              mid' (some-> (state/sub :modal/subsets) (last) (:modal/id))
+              el (rum/deref *ref-el)]
+          (when (or (and (not mid') (= mid modal-id))
+                    (= mid' modal-id))
+            (some-> el (.focus))
+            (js/setTimeout
+              #(some-> (.querySelector el ".shortcut-record-control a.submit")
+                       (.click)) 200))))
+      [modal-life])
+
+    (rum/use-effect!
+      (fn []
+        (let [^js el (rum/deref *ref-el)
+              key-handler (KeyHandler. el)
+
+              teardown-global!
+              (when-not @*global-listener-setup?
+                (shortcut/unlisten-all! true)
+                (reset! *global-listener-setup? true)
+                (fn []
+                  (shortcut/listen-all!)
+                  (reset! *global-listener-setup? false)))]
+
+          ;; setup
+          (events/listen key-handler "key"
+                         (fn [^js e]
+                           (.preventDefault e)
+                           (set-key-conflicts! nil)
+                           (set-keystroke! #(util/trim-safe (str % (shortcut/keyname e))))))
+
+          ;; active
+          (.focus el)
+
+          ;; teardown
+          #(do (some-> teardown-global! (apply nil))
+               (.dispose key-handler)
+               (swap! *customize-modal-life-sentry inc))))
+      [])
+
+    [:div.cp__shortcut-page-x-record-dialog-inner
+     {:class     (util/classnames [{:keypressed keypressed? :dirty dirty?}])
+      :tab-index -1
+      :ref       *ref-el}
+     [:div.sm:w-lsm
+      [:h1.text-2xl.pb-2
+       (t :keymap/customize-for-label)]
+
+      [:p.mb-4.text-md [:b action-name]]
+
+      [:div.shortcuts-keys-wrap
+       [:span.keyboard-shortcut.flex.flex-wrap.mr-2.space-x-2
+        (for [x current-binding]
+          [:code.tracking-wider
+           (-> x (string/trim) (string/lower-case) (shortcut-utils/decorate-binding))
+           [:a.x {:on-click (fn [] (set-current-binding!
+                                     (->> current-binding (remove #(= x %)) (into []))))}
+            (ui/icon "x" {:size 12})]])]
+
+       ;; add shortcut
+       [:div.shortcut-record-control
+        ;; keypressed state
+        (if keypressed?
+          [:<>
+           (when-not (string/blank? keystroke)
+             (ui/render-keyboard-shortcut [keystroke]))
+
+           [:a.flex.items-center.active:opacity-90.submit
+            {:on-click save-keystroke-fn!}
+            (ui/icon "check" {:size 14})]
+           [:a.flex.items-center.text-red-600.hover:text-red-700.active:opacity-90.cancel
+            {:on-click (fn []
+                         (set-keystroke! "")
+                         (set-key-conflicts! nil))}
+            (ui/icon "x" {:size 14})]]
+
+          [:code.flex.items-center
+           [:small.pr-1 (t :keymap/keystroke-record-setup-label)] (ui/icon "keyboard" {:size 14})])]]]
+
+     ;; conflicts results
+     (when (seq key-conflicts)
+       (shortcut-conflicts-display k key-conflicts))
+
+     [:div.action-btns.text-right.mt-6.flex.justify-between.items-center
+      ;; restore default
+      (when (sequential? binding)
+        [:a.flex.items-center.space-x-1.text-sm.opacity-70.hover:opacity-100
+         {:on-click #(set-current-binding! binding)}
+         (t :keymap/restore-to-default)
+         (for [it (some->> binding (map #(some->> % (dh/mod-key) (shortcut-utils/decorate-binding))))]
+           [:span.keyboard-shortcut.ml-1 [:code it]])])
+
+      [:span
+       (ui/button
+         (t :save)
+         :disabled (not dirty?)
+         :on-click (fn []
+                     ;; TODO: check conflicts for the single same leader key
+                     (let [binding' (if (nil? current-binding) [] current-binding)
+                           conflicts (dh/get-conflicts-by-keys binding' handler-id {:exclude-ids #{k}})]
+                       (if (seq conflicts)
+                         (set-key-conflicts! conflicts)
+                         (let [binding' (if (= binding binding') nil binding')]
+                           (shortcut/persist-user-shortcut! k binding')
+                           ;(notification/show! "Saved!" :success)
+                           (state/close-modal!)
+                           (saved-cb))))))
+
+       [:a.reset-btn
+        {:on-click (fn [] (set-current-binding! (or user-binding binding)))}
+        (t :reset)]]]]))
+
+(defn build-categories-map
+  []
+  (->> categories
+       (map #(vector % (into (sorted-map) (dh/binding-by-category %))))))
+
+(rum/defc ^:large-vars/cleanup-todo shortcut-keymap-x
+  []
+  (let [_ (r/use-atom shortcut-config/*category)
+        _ (r/use-atom *refresh-sentry)
+        [ready?, set-ready!] (rum/use-state false)
+        [filters, set-filters!] (rum/use-state #{})
+        [keystroke, set-keystroke!] (rum/use-state "")
+        [q set-q!] (rum/use-state nil)
+
+        categories-list-map (build-categories-map)
+        all-categories (into #{} (map first categories-list-map))
+        in-filters? (boolean (seq filters))
+        in-query? (not (string/blank? (util/trim-safe q)))
+        in-keystroke? (not (string/blank? keystroke))
+
+        [folded-categories set-folded-categories!] (rum/use-state #{})
+
+        matched-list-map
+        (when (and in-query? (not in-keystroke?))
+          (->> categories-list-map
+               (map (fn [[c binding-map]]
+                      [c (search/fuzzy-search
+                           binding-map q
+                           :extract-fn
+                           #(let [[id m] %]
+                              (str (name id) " " (dh/get-shortcut-desc (assoc m :id id)))))]))))
+
+        result-list-map (or matched-list-map categories-list-map)
+        toggle-categories! #(if (= folded-categories all-categories)
+                              (set-folded-categories! #{})
+                              (set-folded-categories! all-categories))]
+
+    (rum/use-effect!
+      (fn []
+        (js/setTimeout #(set-ready! true) 100))
+      [])
+
+    [:div.cp__shortcut-page-x
+     [:header.relative
+      [:h2.text-xs.opacity-70
+       (str (t :keymap/total)
+            " "
+            (if ready?
+              (apply + (map #(count (second %)) result-list-map))
+              " ..."))]
+
+      (pane-controls q set-q! filters set-filters! keystroke set-keystroke! toggle-categories!)]
+
+     [:article
+      (when-not ready?
+        [:p.py-8.flex.justify-center (ui/loading "")])
+
+      (when ready?
+        [:ul.list-none.m-0.py-3
+         (for [[c binding-map] result-list-map
+               :let [folded? (contains? folded-categories c)]]
+           [:<>
+            ;; category row
+            (when (and (not in-query?)
+                       (not in-filters?)
+                       (not in-keystroke?))
+              [:li.flex.justify-between.th
+               {:key      (str c)
+                :on-click #(let [f (if folded? disj conj)]
+                             (set-folded-categories! (f folded-categories c)))}
+               [:strong.font-semibold (t c)]
+               [:i.flex.items-center
+                (ui/icon (if folded? "chevron-left" "chevron-down"))]])
+
+            ;; binding row
+            (when (or in-query? in-filters? (not folded?))
+              (for [[id {:keys [binding user-binding] :as m}] binding-map
+                    :let [binding (to-vector binding)
+                          user-binding (and user-binding (to-vector user-binding))
+                          label (shortcut-desc-label id m)
+                          custom? (not (nil? user-binding))
+                          disabled? (or (false? user-binding)
+                                        (false? (first binding)))
+                          unset? (and (not disabled?)
+                                      (= user-binding []))]]
+
+                (when (or (nil? (seq filters))
+                          (when (contains? filters :Custom) custom?)
+                          (when (contains? filters :Disabled) disabled?)
+                          (when (contains? filters :Unset) unset?))
+
+                  ;; keystrokes filter
+                  (when (or (not in-keystroke?)
+                            (and (not disabled?)
+                                 (not unset?)
+                                 (let [binding' (or user-binding binding)
+                                       keystroke' (some-> (shortcut-utils/safe-parse-string-binding keystroke) (bean/->clj))]
+                                   (when (sequential? binding')
+                                     (some #(when-let [s (some-> % (dh/mod-key) (shortcut-utils/safe-parse-string-binding) (bean/->clj))]
+                                              (or (= s keystroke')
+                                                  (and (sequential? s) (sequential? keystroke')
+                                                       (apply = (map first [s keystroke']))))) binding')))))
+
+                    [:li.flex.items-center.justify-between.text-sm
+                     {:key (str id)}
+                     [:span.label-wrap label]
+
+                     [:a.action-wrap
+                      {:class    (util/classnames [{:disabled disabled?}])
+                       :on-click (when-not disabled?
+                                   #(open-customize-shortcut-dialog! id))}
+
+                      (cond
+                        (or user-binding (false? user-binding))
+                        [:code.dark:bg-green-800.bg-green-300
+                         (if unset?
+                           (t :keymap/unset)
+                           (str (t :keymap/custom) ": "
+                                (if disabled?
+                                  (t :keymap/disabled)
+                                  (bean/->js
+                                    (map #(if (false? %)
+                                            (t :keymap/disabled)
+                                            (shortcut-utils/decorate-binding %)) user-binding)))))]
+
+                        (not unset?)
+                        (for [x binding]
+                          [:code.tracking-wide
+                           {:key (str x)}
+                           (dh/binding-for-display id x)]))]]))))])])]]))

+ 1 - 1
src/main/frontend/components/theme.cljs

@@ -92,7 +92,7 @@
     (rum/use-effect!
      #(state/set-modal!
        (when settings-open?
-         (fn [] [:div.settings-modal (settings/settings)])))
+         (fn [] [:div.settings-modal (settings/settings settings-open?)])))
      [settings-open?])
 
     (rum/use-effect!

+ 1 - 1
src/main/frontend/components/whiteboard.cljs

@@ -292,7 +292,7 @@
      (tldraw-app page-name block-id)]))
 
 (rum/defc whiteboard-route <
-(shortcut/mixin :shortcut.handler/whiteboard)
+(shortcut/mixin :shortcut.handler/whiteboard false)
   [route-match]
   (let [name (get-in route-match [:parameters :path :name])
         {:keys [block-id]} (get-in route-match [:parameters :query])]

+ 9 - 4
src/main/frontend/extensions/pdf/core.cljs

@@ -988,6 +988,11 @@
                             {:set-dirty-hls! set-dirty-hls!
                              :set-hls-extra! set-hls-extra!}) "pdf-viewer")])))])))
 
+(rum/defc pdf-container-outer
+  < (shortcut/mixin :shortcut.handler/pdf false)
+  [child]
+  [:<> child])
+
 (rum/defc pdf-container
   [{:keys [identity] :as pdf-current}]
   (let [[prepared set-prepared!] (rum/use-state false)
@@ -1029,8 +1034,7 @@
 
 (rum/defcs default-embed-playground
   < rum/static rum/reactive
-    (shortcut/mixin :shortcut.handler/pdf)
-  []
+  [state]
   (let [pdf-current (state/sub :pdf/current)
         system-win? (state/sub :pdf/system-win?)]
     [:div.extensions__pdf-playground
@@ -1040,8 +1044,9 @@
 
      (when (and (not system-win?) pdf-current)
        (js/ReactDOM.createPortal
-        (pdf-container pdf-current)
-        (js/document.querySelector "#app-single-container")))]))
+         (pdf-container-outer
+           (pdf-container pdf-current))
+         (js/document.querySelector "#app-single-container")))]))
 
 (rum/defcs system-embed-playground
   < rum/reactive

+ 1 - 1
src/main/frontend/extensions/srs.cljs

@@ -512,7 +512,7 @@
            [:div.my-3 (ui/button "Review cards" :small? true)])]))))
 
 (rum/defc view-modal <
-  (shortcut/mixin :shortcut.handler/cards)
+  (shortcut/mixin :shortcut.handler/cards false)
   [blocks option card-index]
   [:div#cards-modal
    (if (seq blocks)

+ 3 - 2
src/main/frontend/handler/command_palette.cljs

@@ -2,6 +2,7 @@
   "System-component-like ns for command palette's functionality"
   (:require [cljs.spec.alpha :as s]
             [frontend.modules.shortcut.data-helper :as shortcut-helper]
+            [frontend.handler.plugin :as plugin-handler]
             [frontend.spec :as spec]
             [frontend.state :as state]
             [lambdaisland.glogi :as log]
@@ -50,10 +51,10 @@
 (defn add-history [{:keys [id]}]
   (storage/set "commands-history" (conj (history) {:id id :timestamp (.getTime (js/Date.))})))
 
-(defn invoke-command [{:keys [action] :as cmd}]
+(defn invoke-command [{:keys [id action] :as cmd}]
   (add-history cmd)
   (state/close-modal!)
-  (action))
+  (plugin-handler/hook-lifecycle-fn! id action))
 
 (defn top-commands [limit]
   (->> (get-commands)

+ 3 - 0
src/main/frontend/handler/config.cljs

@@ -18,6 +18,9 @@
       (repo-config-handler/read-repo-config content)
       (let [result (parse-repo-config content)
             ks (if (vector? k) k [k])
+            v (cond->> v
+                       (map? v)
+                       (reduce-kv (fn [a k v] (rewrite/assoc a k v)) (rewrite/parse-string "{}")))
             new-result (rewrite/assoc-in result ks v)
             new-content (str new-result)]
         (file-handler/set-file-content! repo path new-content) nil))))

+ 4 - 7
src/main/frontend/handler/events.cljs

@@ -22,7 +22,6 @@
             [frontend.components.shell :as shell]
             [frontend.components.whiteboard :as whiteboard]
             [frontend.components.user.login :as login]
-            [frontend.components.shortcut :as shortcut]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
@@ -457,8 +456,8 @@
   (commands/exec-plugin-simple-command! pid cmd action))
 
 (defmethod handle :shortcut-handler-refreshed [[_]]
-  (when-not @st/*inited?
-    (reset! st/*inited? true)
+  (when-not @st/*pending-inited?
+    (reset! st/*pending-inited? true)
     (st/consume-pending-shortcuts!)))
 
 (defmethod handle :mobile/keyboard-will-show [[_ keyboard-height]]
@@ -936,10 +935,8 @@
 (defmethod handle :editor/quick-capture [[_ args]]
   (quick-capture/quick-capture args))
 
-(defmethod handle :modal/keymap-manager [[_]]
-  (state/set-modal!
-    #(shortcut/keymap-pane)
-    {:label "keymap-manager"}))
+(defmethod handle :modal/keymap [[_]]
+  (state/open-settings! :keymap))
 
 (defmethod handle :editor/toggle-own-number-list [[_ blocks]]
   (let [batch? (sequential? blocks)

+ 18 - 1
src/main/frontend/handler/global_config.cljs

@@ -8,6 +8,7 @@
             [shadow.resource :as rc]
             [clojure.edn :as edn]
             [electron.ipc :as ipc]
+            [borkdude.rewrite-edn :as rewrite]
             [logseq.common.path :as path]))
 
 ;; Use defonce to avoid broken state on dev reload
@@ -38,7 +39,7 @@
 (defn set-global-config-state!
   [content]
   (let [config (edn/read-string content)]
-    (state/set-global-config! config)
+    (state/set-global-config! config content)
     config))
 
 (def default-content (rc/inline "templates/global-config.edn"))
@@ -59,6 +60,22 @@
     (p/let [config-content (fs/read-file nil config-path)]
            (set-global-config-state! config-content))))
 
+(defn set-global-config-kv!
+  [k v]
+  (let [result (rewrite/parse-string
+                 (or (state/get-global-config-str-content) "{}"))
+        ks (if (sequential? k) k [k])
+        v (cond->> v
+                   (map? v)
+                   (reduce-kv (fn [a k v] (rewrite/assoc a k v)) (rewrite/parse-string "{}")))
+        new-result (if (and (= 1 (count ks))
+                            (nil? v))
+                     (rewrite/dissoc result (first ks))
+                     (rewrite/assoc-in result ks v))
+        new-str-content (str new-result)]
+    (fs/write-file! nil nil (global-config-path) new-str-content {:skip-compare? true})
+    (state/set-global-config! (rewrite/sexpr new-result) new-str-content)))
+
 (defn start
   "This component has four responsibilities on start:
 - Fetch root-dir for later use with config paths

+ 5 - 3
src/main/frontend/handler/notification.cljs

@@ -18,11 +18,13 @@
   ([content]
    (show! content :info true nil 2000 nil))
   ([content status]
-   (show! content status true nil 1500 nil))
+   (show! content status (not= status :error) nil 1500 nil))
   ([content status clear?]
    (show! content status clear? nil 1500 nil))
   ([content status clear? uid]
    (show! content status clear? uid 1500 nil))
+  ([content status clear? uid timeout]
+   (show! content status clear? uid timeout nil))
   ([content status clear? uid timeout close-cb]
    (let [contents (state/get-notification-contents)
          uid (or uid (keyword (util/unique-id)))]
@@ -31,7 +33,7 @@
                                                           :status status
                                                           :close-cb close-cb}))
 
-     (when (and clear? (not= status :error))
-       (js/setTimeout #(clear! uid) (or timeout 1500)))
+     (when (and clear? (or timeout (not= status :error)))
+       (js/setTimeout #(clear! uid) (or timeout 2000)))
 
      uid)))

+ 12 - 3
src/main/frontend/handler/plugin.cljs

@@ -7,6 +7,7 @@
             [logseq.graph-parser.mldoc :as gp-mldoc]
             [frontend.handler.notification :as notification]
             [frontend.handler.common.plugin :as plugin-common-handler]
+            [frontend.modules.shortcut.utils :as shortcut-utils]
             [frontend.storage :as storage]
             [camel-snake-kebab.core :as csk]
             [frontend.state :as state]
@@ -175,7 +176,7 @@
 
 (defn has-setting-schema?
   [id]
-  (when-let [pl (and id (get-plugin-inst (name id)))]
+  (when-let [^js pl (and id (get-plugin-inst (name id)))]
     (boolean (.-settingsSchema pl))))
 
 (defn get-enabled-plugins-if-setting-schema
@@ -297,7 +298,7 @@
   (let [id      (keyword (str "plugin." pid "/" key))
         binding (:binding keybinding)
         binding (some->> (if (string? binding) [binding] (seq binding))
-                         (map util/normalize-user-keyname))
+                         (map shortcut-utils/undecorate-binding))
         binding (if util/mac?
                   (or (:mac keybinding) binding) binding)
         mode    (or (:mode keybinding) :global)
@@ -658,6 +659,15 @@
                        :remove disj)]
       (save-plugin-preferences! {:pinnedToolbarItems (op-fn pinned (name key))}))))
 
+(defn hook-lifecycle-fn!
+  [type f & args]
+  (when (and type (fn? f))
+    (when config/lsp-enabled?
+      (hook-plugin-app (str :before-command-invoked type) nil))
+    (apply f args)
+    (when config/lsp-enabled?
+      (hook-plugin-app (str :after-command-invoked type) nil))))
+
 ;; components
 (rum/defc lsp-indicator < rum/reactive
   []
@@ -788,7 +798,6 @@
     (callback)
     (init-plugins! callback)))
 
-
 (comment
   {:pending        (count (:plugin/updates-pending @state/state))
    :auto-checking? (boolean (:plugin/updates-auto-checking? @state/state))

+ 26 - 24
src/main/frontend/mixins.cljs

@@ -29,31 +29,33 @@
 
 (defn hide-when-esc-or-outside
   [state & {:keys [on-hide node visibilitychange? outside?]}]
-  (try
-    (let [dom-node (rum/dom-node state)]
-      (when-let [dom-node (or node dom-node)]
-        (let [click-fn (fn [e]
-                         (let [target (.. e -target)]
-                           ;; If the click target is outside of current node
-                           (when (and
-                                  (not (dom/contains dom-node target))
-                                  (not (.contains (.-classList target) "ignore-outside-event")))
-                             (on-hide state e :click))))]
-          (when-not (false? outside?)
-            (listen state js/window "mousedown" click-fn)))
-        (listen state js/window "keydown"
-                (fn [e]
-                  (case (.-keyCode e)
-                    ;; Esc
-                    27 (on-hide state e :esc)
-                    nil)))
-        (when visibilitychange?
-          (listen state js/window "visibilitychange"
+  (let [opts (last (:rum/args state))
+        outside? (cond-> opts (nil? outside?) (:outside?))]
+    (try
+      (let [dom-node (rum/dom-node state)]
+        (when-let [dom-node (or node dom-node)]
+          (let [click-fn (fn [e]
+                           (let [target (.. e -target)]
+                             ;; If the click target is outside of current node
+                             (when (and
+                                     (not (dom/contains dom-node target))
+                                     (not (.contains (.-classList target) "ignore-outside-event")))
+                               (on-hide state e :click))))]
+            (when-not (false? outside?)
+              (listen state js/window "mousedown" click-fn)))
+          (listen state js/window "keydown"
                   (fn [e]
-                    (on-hide state e :visibilitychange))))))
-    (catch :default _e
-      ;; TODO: Unable to find node on an unmounted component.
-      nil)))
+                    (case (.-keyCode e)
+                      ;; Esc
+                      27 (on-hide state e :esc)
+                      nil)))
+          (when visibilitychange?
+            (listen state js/window "visibilitychange"
+                    (fn [e]
+                      (on-hide state e :visibilitychange))))))
+      (catch :default _e
+        ;; TODO: Unable to find node on an unmounted component.
+        nil))))
 
 (defn on-enter
   [state & {:keys [on-enter node]}]

+ 673 - 696
src/main/frontend/modules/shortcut/config.cljs

@@ -1,5 +1,6 @@
 (ns frontend.modules.shortcut.config
-  (:require [frontend.components.commit :as commit]
+  (:require [clojure.string :as str]
+            [frontend.components.commit :as commit]
             [frontend.extensions.srs.handler :as srs]
             [frontend.extensions.pdf.utils :as pdf-utils]
             [frontend.handler.config :as config-handler]
@@ -40,207 +41,207 @@
 ;;  * :fn - Fn or a qualified keyword that represents a fn
 ;;  * :inactive - Optional boolean to disable a shortcut for certain conditions
 ;;    e.g. a given platform or feature condition
-(def ^:large-vars/data-var all-default-keyboard-shortcuts
+(def ^:large-vars/data-var all-built-in-keyboard-shortcuts
   ;; BUG: Actually, "enter" is registered by mixin behind a "when inputing" guard
   ;; So this setting item does not cover all cases.
   ;; See-also: frontend.components.datetime/time-repeater
-  {:date-picker/complete         {:binding "enter"
-                                  :fn      ui-handler/shortcut-complete}
+  {:date-picker/complete                    {:binding "enter"
+                                             :fn      ui-handler/shortcut-complete}
 
-   :date-picker/prev-day         {:binding "left"
-                                  :fn      ui-handler/shortcut-prev-day}
+   :date-picker/prev-day                    {:binding "left"
+                                             :fn      ui-handler/shortcut-prev-day}
 
-   :date-picker/next-day         {:binding "right"
-                                  :fn      ui-handler/shortcut-next-day}
+   :date-picker/next-day                    {:binding "right"
+                                             :fn      ui-handler/shortcut-next-day}
 
-   :date-picker/prev-week        {:binding ["up" "ctrl+p"]
-                                  :fn      ui-handler/shortcut-prev-week}
+   :date-picker/prev-week                   {:binding ["up" "ctrl+p"]
+                                             :fn      ui-handler/shortcut-prev-week}
 
-   :date-picker/next-week        {:binding ["down" "ctrl+n"]
-                                  :fn      ui-handler/shortcut-next-week}
+   :date-picker/next-week                   {:binding ["down" "ctrl+n"]
+                                             :fn      ui-handler/shortcut-next-week}
 
-   :pdf/previous-page            {:binding "alt+p"
-                                  :fn      pdf-utils/prev-page}
+   :pdf/previous-page                       {:binding "alt+p"
+                                             :fn      pdf-utils/prev-page}
 
-   :pdf/next-page                {:binding "alt+n"
-                                  :fn      pdf-utils/next-page}
+   :pdf/next-page                           {:binding "alt+n"
+                                             :fn      pdf-utils/next-page}
 
-   :pdf/close                    {:binding "alt+x"
-                                  :fn      #(state/set-state! :pdf/current nil)}
+   :pdf/close                               {:binding "alt+x"
+                                             :fn      #(state/set-state! :pdf/current nil)}
 
-   :pdf/find                     {:binding "alt+f"
-                                  :fn      pdf-utils/open-finder}
+   :pdf/find                                {:binding "alt+f"
+                                             :fn      pdf-utils/open-finder}
 
-   :whiteboard/select            {:binding ["1" "w s"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "select")}
-   
-   :whiteboard/pan               {:binding ["2" "w p"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "move")}
-   
-   :whiteboard/portal            {:binding ["3" "w b"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "logseq-portal")}
+   :whiteboard/select                       {:binding ["1" "w s"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "select")}
 
-   :whiteboard/pencil            {:binding ["4" "w d"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "pencil")}
+   :whiteboard/pan                          {:binding ["2" "w p"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "move")}
 
-   :whiteboard/highlighter       {:binding ["5" "w h"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "highlighter")}
-   
-   :whiteboard/eraser            {:binding ["6" "w e"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "erase")}
-   
-   :whiteboard/connector         {:binding ["7" "w c"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "line")}
-   
-   :whiteboard/text              {:binding ["8" "w t"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "text")}
+   :whiteboard/portal                       {:binding ["3" "w b"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "logseq-portal")}
 
-   :whiteboard/rectangle         {:binding ["9" "w r"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "box")}
+   :whiteboard/pencil                       {:binding ["4" "w d"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "pencil")}
 
-   :whiteboard/ellipse           {:binding ["o" "w o"]
-                                  :fn      #(.selectTool ^js (state/active-tldraw-app) "ellipse")}
+   :whiteboard/highlighter                  {:binding ["5" "w h"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "highlighter")}
 
-   :whiteboard/reset-zoom        {:binding "shift+0"
-                                  :fn      #(.resetZoom (.-api ^js (state/active-tldraw-app)))}
+   :whiteboard/eraser                       {:binding ["6" "w e"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "erase")}
 
-   :whiteboard/zoom-to-fit       {:binding "shift+1"
-                                  :fn      #(.zoomToFit (.-api ^js (state/active-tldraw-app)))}
+   :whiteboard/connector                    {:binding ["7" "w c"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "line")}
 
-   :whiteboard/zoom-to-selection {:binding "shift+2"
-                                  :fn      #(.zoomToSelection (.-api ^js (state/active-tldraw-app)))}
+   :whiteboard/text                         {:binding ["8" "w t"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "text")}
 
-   :whiteboard/zoom-out          {:binding "shift+dash"
-                                  :fn      #(.zoomOut (.-api ^js (state/active-tldraw-app)) false)}
+   :whiteboard/rectangle                    {:binding ["9" "w r"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "box")}
 
-   :whiteboard/zoom-in           {:binding "shift+equals"
-                                  :fn      #(.zoomIn (.-api ^js (state/active-tldraw-app)) false)}
+   :whiteboard/ellipse                      {:binding ["o" "w o"]
+                                             :fn      #(.selectTool ^js (state/active-tldraw-app) "ellipse")}
 
-   :whiteboard/send-backward     {:binding "open-square-bracket"
-                                  :fn      #(.sendBackward ^js (state/active-tldraw-app))}
+   :whiteboard/reset-zoom                   {:binding "shift+0"
+                                             :fn      #(.resetZoom (.-api ^js (state/active-tldraw-app)))}
 
-   :whiteboard/send-to-back      {:binding "shift+open-square-bracket"
-                                  :fn      #(.sendToBack ^js (state/active-tldraw-app))}
+   :whiteboard/zoom-to-fit                  {:binding "shift+1"
+                                             :fn      #(.zoomToFit (.-api ^js (state/active-tldraw-app)))}
 
-   :whiteboard/bring-forward     {:binding "close-square-bracket"
-                                  :fn      #(.bringForward ^js (state/active-tldraw-app))}
+   :whiteboard/zoom-to-selection            {:binding "shift+2"
+                                             :fn      #(.zoomToSelection (.-api ^js (state/active-tldraw-app)))}
 
-   :whiteboard/bring-to-front    {:binding "shift+close-square-bracket"
-                                  :fn      #(.bringToFront ^js (state/active-tldraw-app))}
+   :whiteboard/zoom-out                     {:binding "shift+dash"
+                                             :fn      #(.zoomOut (.-api ^js (state/active-tldraw-app)) false)}
 
-   :whiteboard/lock              {:binding "mod+l"
-                                  :fn      #(.setLocked ^js (state/active-tldraw-app) true)}
+   :whiteboard/zoom-in                      {:binding "shift+equals"
+                                             :fn      #(.zoomIn (.-api ^js (state/active-tldraw-app)) false)}
 
-   :whiteboard/unlock            {:binding "mod+shift+l"
-                                  :fn      #(.setLocked ^js (state/active-tldraw-app) false)}
+   :whiteboard/send-backward                {:binding "open-square-bracket"
+                                             :fn      #(.sendBackward ^js (state/active-tldraw-app))}
 
-   :whiteboard/group             {:binding "mod+g"
-                                  :fn      #(.doGroup (.-api ^js (state/active-tldraw-app)))}
+   :whiteboard/send-to-back                 {:binding "shift+open-square-bracket"
+                                             :fn      #(.sendToBack ^js (state/active-tldraw-app))}
 
-   :whiteboard/ungroup           {:binding "mod+shift+g"
-                                  :fn      #(.unGroup (.-api ^js (state/active-tldraw-app)))}
+   :whiteboard/bring-forward                {:binding "close-square-bracket"
+                                             :fn      #(.bringForward ^js (state/active-tldraw-app))}
 
-   :whiteboard/toggle-grid       {:binding "t g"
-                                  :fn      #(.toggleGrid (.-api ^js (state/active-tldraw-app)))}
+   :whiteboard/bring-to-front               {:binding "shift+close-square-bracket"
+                                             :fn      #(.bringToFront ^js (state/active-tldraw-app))}
 
-   :auto-complete/complete       {:binding "enter"
-                                  :fn      ui-handler/auto-complete-complete}
+   :whiteboard/lock                         {:binding "mod+l"
+                                             :fn      #(.setLocked ^js (state/active-tldraw-app) true)}
 
-   :auto-complete/prev           {:binding ["up" "ctrl+p"]
-                                  :fn      ui-handler/auto-complete-prev}
+   :whiteboard/unlock                       {:binding "mod+shift+l"
+                                             :fn      #(.setLocked ^js (state/active-tldraw-app) false)}
 
-   :auto-complete/next           {:binding ["down" "ctrl+n"]
-                                  :fn      ui-handler/auto-complete-next}
+   :whiteboard/group                        {:binding "mod+g"
+                                             :fn      #(.doGroup (.-api ^js (state/active-tldraw-app)))}
 
-   :auto-complete/shift-complete {:binding "shift+enter"
-                                  :fn      ui-handler/auto-complete-shift-complete}
+   :whiteboard/ungroup                      {:binding "mod+shift+g"
+                                             :fn      #(.unGroup (.-api ^js (state/active-tldraw-app)))}
 
-   :auto-complete/open-link      {:binding "mod+o"
-                                  :fn      ui-handler/auto-complete-open-link}
+   :whiteboard/toggle-grid                  {:binding "t g"
+                                             :fn      #(.toggleGrid (.-api ^js (state/active-tldraw-app)))}
 
-   :cards/toggle-answers         {:binding "s"
-                                  :fn      srs/toggle-answers}
+   :auto-complete/complete                  {:binding "enter"
+                                             :fn      ui-handler/auto-complete-complete}
 
-   :cards/next-card              {:binding "n"
-                                  :fn      srs/next-card}
+   :auto-complete/prev                      {:binding ["up" "ctrl+p"]
+                                             :fn      ui-handler/auto-complete-prev}
 
-   :cards/forgotten              {:binding "f"
-                                  :fn      srs/forgotten}
+   :auto-complete/next                      {:binding ["down" "ctrl+n"]
+                                             :fn      ui-handler/auto-complete-next}
 
-   :cards/remembered             {:binding "r"
-                                  :fn      srs/remembered}
+   :auto-complete/shift-complete            {:binding "shift+enter"
+                                             :fn      ui-handler/auto-complete-shift-complete}
 
-   :cards/recall                 {:binding "t"
-                                  :fn      srs/recall}
+   :auto-complete/open-link                 {:binding "mod+o"
+                                             :fn      ui-handler/auto-complete-open-link}
 
-   :editor/escape-editing        {:binding false
-                                  :fn      (fn [_ _]
-                                             (editor-handler/escape-editing))}
+   :cards/toggle-answers                    {:binding "s"
+                                             :fn      srs/toggle-answers}
 
-   :editor/backspace             {:binding "backspace"
-                                  :fn      editor-handler/editor-backspace}
+   :cards/next-card                         {:binding "n"
+                                             :fn      srs/next-card}
 
-   :editor/delete                {:binding "delete"
-                                  :fn      editor-handler/editor-delete}
+   :cards/forgotten                         {:binding "f"
+                                             :fn      srs/forgotten}
 
-   :editor/new-block             {:binding "enter"
-                                  :fn      editor-handler/keydown-new-block-handler}
+   :cards/remembered                        {:binding "r"
+                                             :fn      srs/remembered}
 
-   :editor/new-line              {:binding "shift+enter"
-                                  :fn      editor-handler/keydown-new-line-handler}
+   :cards/recall                            {:binding "t"
+                                             :fn      srs/recall}
 
-   :editor/new-whiteboard        {:binding "n w"
-                                  :fn      #(whiteboard-handler/create-new-whiteboard-and-redirect!)}
+   :editor/escape-editing                   {:binding false
+                                             :fn      (fn [_ _]
+                                                        (editor-handler/escape-editing))}
 
-   :editor/follow-link           {:binding "mod+o"
-                                  :fn      editor-handler/follow-link-under-cursor!}
+   :editor/backspace                        {:binding "backspace"
+                                             :fn      editor-handler/editor-backspace}
 
-   :editor/open-link-in-sidebar  {:binding "mod+shift+o"
-                                  :fn      editor-handler/open-link-in-sidebar!}
+   :editor/delete                           {:binding "delete"
+                                             :fn      editor-handler/editor-delete}
 
-   :editor/bold                  {:binding "mod+b"
-                                  :fn      editor-handler/bold-format!}
+   :editor/new-block                        {:binding "enter"
+                                             :fn      editor-handler/keydown-new-block-handler}
 
-   :editor/italics               {:binding "mod+i"
-                                  :fn      editor-handler/italics-format!}
+   :editor/new-line                         {:binding "shift+enter"
+                                             :fn      editor-handler/keydown-new-line-handler}
 
-   :editor/highlight             {:binding "mod+shift+h"
-                                  :fn      editor-handler/highlight-format!}
+   :editor/new-whiteboard                   {:binding "n w"
+                                             :fn      #(whiteboard-handler/create-new-whiteboard-and-redirect!)}
 
-   :editor/strike-through        {:binding "mod+shift+s"
-                                  :fn      editor-handler/strike-through-format!}
+   :editor/follow-link                      {:binding "mod+o"
+                                             :fn      editor-handler/follow-link-under-cursor!}
 
-   :editor/clear-block           {:binding (if mac? "ctrl+l" "alt+l")
-                                  :fn      editor-handler/clear-block-content!}
+   :editor/open-link-in-sidebar             {:binding "mod+shift+o"
+                                             :fn      editor-handler/open-link-in-sidebar!}
 
-   :editor/kill-line-before      {:binding (if mac? "ctrl+u" "alt+u")
-                                  :fn      editor-handler/kill-line-before!}
+   :editor/bold                             {:binding "mod+b"
+                                             :fn      editor-handler/bold-format!}
 
-   :editor/kill-line-after       {:binding (if mac? false "alt+k")
-                                  :fn      editor-handler/kill-line-after!}
+   :editor/italics                          {:binding "mod+i"
+                                             :fn      editor-handler/italics-format!}
 
-   :editor/beginning-of-block    {:binding (if mac? false "alt+a")
-                                  :fn      editor-handler/beginning-of-block}
+   :editor/highlight                        {:binding "mod+shift+h"
+                                             :fn      editor-handler/highlight-format!}
 
-   :editor/end-of-block          {:binding (if mac? false "alt+e")
-                                  :fn      editor-handler/end-of-block}
+   :editor/strike-through                   {:binding "mod+shift+s"
+                                             :fn      editor-handler/strike-through-format!}
 
-   :editor/forward-word          {:binding (if mac? "ctrl+shift+f" "alt+f")
-                                  :fn      editor-handler/cursor-forward-word}
+   :editor/clear-block                      {:binding (if mac? "ctrl+l" "alt+l")
+                                             :fn      editor-handler/clear-block-content!}
 
-   :editor/backward-word         {:binding (if mac? "ctrl+shift+b" "alt+b")
-                                  :fn      editor-handler/cursor-backward-word}
+   :editor/kill-line-before                 {:binding (if mac? "ctrl+u" "alt+u")
+                                             :fn      editor-handler/kill-line-before!}
 
-   :editor/forward-kill-word     {:binding (if mac? "ctrl+w" "alt+d")
-                                  :fn      editor-handler/forward-kill-word}
+   :editor/kill-line-after                  {:binding (if mac? false "alt+k")
+                                             :fn      editor-handler/kill-line-after!}
 
-   :editor/backward-kill-word    {:binding (if mac? false "alt+w")
-                                  :fn      editor-handler/backward-kill-word}
+   :editor/beginning-of-block               {:binding (if mac? false "alt+a")
+                                             :fn      editor-handler/beginning-of-block}
+
+   :editor/end-of-block                     {:binding (if mac? false "alt+e")
+                                             :fn      editor-handler/end-of-block}
+
+   :editor/forward-word                     {:binding (if mac? "ctrl+shift+f" "alt+f")
+                                             :fn      editor-handler/cursor-forward-word}
+
+   :editor/backward-word                    {:binding (if mac? "ctrl+shift+b" "alt+b")
+                                             :fn      editor-handler/cursor-backward-word}
+
+   :editor/forward-kill-word                {:binding (if mac? "ctrl+w" "alt+d")
+                                             :fn      editor-handler/forward-kill-word}
+
+   :editor/backward-kill-word               {:binding (if mac? false "alt+w")
+                                             :fn      editor-handler/backward-kill-word}
 
    :editor/replace-block-reference-at-point {:binding "mod+shift+r"
                                              :fn      editor-handler/replace-block-reference-with-content-at-point}
-   :editor/copy-embed {:binding "mod+e"
-                       :fn      editor-handler/copy-current-block-embed}
+   :editor/copy-embed                       {:binding "mod+e"
+                                             :fn      editor-handler/copy-current-block-embed}
 
    :editor/paste-text-in-one-block-at-point {:binding "mod+shift+v"
                                              :fn      paste-handler/editor-on-paste-raw!}
@@ -248,299 +249,299 @@
    :editor/insert-youtube-timestamp         {:binding "mod+shift+y"
                                              :fn      commands/insert-youtube-timestamp}
 
-   :editor/cycle-todo              {:binding "mod+enter"
-                                    :fn      editor-handler/cycle-todo!}
+   :editor/cycle-todo                       {:binding "mod+enter"
+                                             :fn      editor-handler/cycle-todo!}
 
-   :editor/up                      {:binding ["up" "ctrl+p"]
-                                    :fn      (editor-handler/shortcut-up-down :up)}
+   :editor/up                               {:binding ["up" "ctrl+p"]
+                                             :fn      (editor-handler/shortcut-up-down :up)}
 
-   :editor/down                    {:binding ["down" "ctrl+n"]
-                                    :fn      (editor-handler/shortcut-up-down :down)}
+   :editor/down                             {:binding ["down" "ctrl+n"]
+                                             :fn      (editor-handler/shortcut-up-down :down)}
 
-   :editor/left                    {:binding "left"
-                                    :fn      (editor-handler/shortcut-left-right :left)}
+   :editor/left                             {:binding "left"
+                                             :fn      (editor-handler/shortcut-left-right :left)}
 
-   :editor/right                   {:binding "right"
-                                    :fn      (editor-handler/shortcut-left-right :right)}
+   :editor/right                            {:binding "right"
+                                             :fn      (editor-handler/shortcut-left-right :right)}
 
-   :editor/move-block-up           {:binding (if mac? "mod+shift+up" "alt+shift+up")
-                                    :fn      (editor-handler/move-up-down true)}
+   :editor/move-block-up                    {:binding (if mac? "mod+shift+up" "alt+shift+up")
+                                             :fn      (editor-handler/move-up-down true)}
 
-   :editor/move-block-down         {:binding (if mac? "mod+shift+down" "alt+shift+down")
-                                    :fn      (editor-handler/move-up-down false)}
+   :editor/move-block-down                  {:binding (if mac? "mod+shift+down" "alt+shift+down")
+                                             :fn      (editor-handler/move-up-down false)}
 
    ;; FIXME: add open edit in non-selection mode
-   :editor/open-edit               {:binding "enter"
-                                    :fn      (partial editor-handler/open-selected-block! :right)}
+   :editor/open-edit                        {:binding "enter"
+                                             :fn      (partial editor-handler/open-selected-block! :right)}
+
+   :editor/select-block-up                  {:binding "alt+up"
+                                             :fn      (editor-handler/on-select-block :up)}
 
-   :editor/select-block-up         {:binding "alt+up"
-                                    :fn      (editor-handler/on-select-block :up)}
+   :editor/select-block-down                {:binding "alt+down"
+                                             :fn      (editor-handler/on-select-block :down)}
 
-   :editor/select-block-down       {:binding "alt+down"
-                                    :fn      (editor-handler/on-select-block :down)}
+   :editor/select-up                        {:binding "shift+up"
+                                             :fn      (editor-handler/shortcut-select-up-down :up)}
 
-   :editor/select-up               {:binding "shift+up"
-                                    :fn      (editor-handler/shortcut-select-up-down :up)}
+   :editor/select-down                      {:binding "shift+down"
+                                             :fn      (editor-handler/shortcut-select-up-down :down)}
 
-   :editor/select-down             {:binding "shift+down"
-                                    :fn      (editor-handler/shortcut-select-up-down :down)}
+   :editor/delete-selection                 {:binding ["backspace" "delete"]
+                                             :fn      editor-handler/delete-selection}
 
-   :editor/delete-selection        {:binding ["backspace" "delete"]
-                                    :fn      editor-handler/delete-selection}
+   :editor/expand-block-children            {:binding "mod+down"
+                                             :fn      editor-handler/expand!}
 
-   :editor/expand-block-children   {:binding "mod+down"
-                                    :fn      editor-handler/expand!}
+   :editor/collapse-block-children          {:binding "mod+up"
+                                             :fn      editor-handler/collapse!}
 
-   :editor/collapse-block-children {:binding "mod+up"
-                                    :fn      editor-handler/collapse!}
+   :editor/indent                           {:binding "tab"
+                                             :fn      (editor-handler/keydown-tab-handler :right)}
 
-   :editor/indent                  {:binding "tab"
-                                    :fn      (editor-handler/keydown-tab-handler :right)}
+   :editor/outdent                          {:binding "shift+tab"
+                                             :fn      (editor-handler/keydown-tab-handler :left)}
 
-   :editor/outdent                 {:binding "shift+tab"
-                                    :fn      (editor-handler/keydown-tab-handler :left)}
+   :editor/copy                             {:binding "mod+c"
+                                             :fn      editor-handler/shortcut-copy}
 
-   :editor/copy                    {:binding "mod+c"
-                                    :fn      editor-handler/shortcut-copy}
+   :editor/copy-text                        {:binding "mod+shift+c"
+                                             :fn      editor-handler/shortcut-copy-text}
 
-   :editor/copy-text               {:binding "mod+shift+c"
-                                    :fn      editor-handler/shortcut-copy-text}
+   :editor/cut                              {:binding "mod+x"
+                                             :fn      editor-handler/shortcut-cut}
 
-   :editor/cut                     {:binding "mod+x"
-                                    :fn      editor-handler/shortcut-cut}
+   :editor/undo                             {:binding "mod+z"
+                                             :fn      history/undo!}
 
-   :editor/undo                    {:binding "mod+z"
-                                    :fn      history/undo!}
+   :editor/redo                             {:binding ["mod+shift+z" "mod+y"]
+                                             :fn      history/redo!}
 
-   :editor/redo                    {:binding ["mod+shift+z" "mod+y"]
-                                    :fn      history/redo!}
+   :editor/insert-link                      {:binding "mod+l"
+                                             :fn      #(editor-handler/html-link-format!)}
 
-   :editor/insert-link             {:binding "mod+l"
-                                    :fn      #(editor-handler/html-link-format!)}
+   :editor/select-all-blocks                {:binding "mod+shift+a"
+                                             :fn      editor-handler/select-all-blocks!}
 
-   :editor/select-all-blocks       {:binding "mod+shift+a"
-                                    :fn      editor-handler/select-all-blocks!}
+   :editor/select-parent                    {:binding "mod+a"
+                                             :fn      editor-handler/select-parent}
 
-   :editor/select-parent           {:binding "mod+a"
-                                    :fn      editor-handler/select-parent}
+   :editor/zoom-in                          {:binding (if mac? "mod+." "alt+right")
+                                             :fn      editor-handler/zoom-in!}
 
-   :editor/zoom-in                 {:binding (if mac? "mod+." "alt+right")
-                                    :fn      editor-handler/zoom-in!}
+   :editor/zoom-out                         {:binding (if mac? "mod+," "alt+left")
+                                             :fn      editor-handler/zoom-out!}
 
-   :editor/zoom-out                {:binding (if mac? "mod+," "alt+left")
-                                    :fn      editor-handler/zoom-out!}
+   :editor/toggle-undo-redo-mode            {:binding false
+                                             :fn      undo-redo/toggle-undo-redo-mode!}
 
-   :editor/toggle-undo-redo-mode   {:binding false
-                                    :fn      undo-redo/toggle-undo-redo-mode!}
-   
-   :editor/toggle-number-list      {:binding "t n"
-                                    :fn #(state/pub-event! [:editor/toggle-own-number-list (state/get-selection-block-ids)])}
+   :editor/toggle-number-list               {:binding "t n"
+                                             :fn      #(state/pub-event! [:editor/toggle-own-number-list (state/get-selection-block-ids)])}
 
-   :ui/toggle-brackets             {:binding "mod+c mod+b"
-                                    :fn      config-handler/toggle-ui-show-brackets!}
+   :ui/toggle-brackets                      {:binding "mod+c mod+b"
+                                             :fn      config-handler/toggle-ui-show-brackets!}
 
-   :go/search-in-page              {:binding "mod+shift+k"
-                                    :fn      #(do
-                                                (editor-handler/escape-editing)
-                                                (route-handler/go-to-search! :page))}
+   :go/search-in-page                       {:binding "mod+shift+k"
+                                             :fn      #(do
+                                                         (editor-handler/escape-editing)
+                                                         (route-handler/go-to-search! :page))}
 
-   :go/search                      {:binding "mod+k"
-                                    :fn      #(do
-                                                (editor-handler/escape-editing false)
-                                                (route-handler/go-to-search! :global))}
+   :go/search                               {:binding "mod+k"
+                                             :fn      #(do
+                                                         (editor-handler/escape-editing false)
+                                                         (route-handler/go-to-search! :global))}
 
-   :go/electron-find-in-page       {:binding "mod+f"
-                                    :inactive (not (util/electron?))
-                                    :fn      #(search-handler/open-find-in-page!)}
+   :go/electron-find-in-page                {:binding  "mod+f"
+                                             :inactive (not (util/electron?))
+                                             :fn       #(search-handler/open-find-in-page!)}
 
-   :go/electron-jump-to-the-next {:binding ["enter" "mod+g"]
-                                  :inactive (not (util/electron?))
-                                  :fn      #(search-handler/loop-find-in-page! false)}
+   :go/electron-jump-to-the-next            {:binding  ["enter" "mod+g"]
+                                             :inactive (not (util/electron?))
+                                             :fn       #(search-handler/loop-find-in-page! false)}
 
-   :go/electron-jump-to-the-previous {:binding ["shift+enter" "mod+shift+g"]
-                                      :inactive (not (util/electron?))
-                                      :fn      #(search-handler/loop-find-in-page! true)}
+   :go/electron-jump-to-the-previous        {:binding  ["shift+enter" "mod+shift+g"]
+                                             :inactive (not (util/electron?))
+                                             :fn       #(search-handler/loop-find-in-page! true)}
 
-   :go/journals                    {:binding "g j"
-                                    :fn      route-handler/go-to-journals!}
+   :go/journals                             {:binding "g j"
+                                             :fn      route-handler/go-to-journals!}
 
-   :go/backward                    {:binding "mod+open-square-bracket"
-                                    :fn      (fn [_] (js/window.history.back))}
+   :go/backward                             {:binding "mod+open-square-bracket"
+                                             :fn      (fn [_] (js/window.history.back))}
 
-   :go/forward                     {:binding "mod+close-square-bracket"
-                                    :fn      (fn [_] (js/window.history.forward))}
+   :go/forward                              {:binding "mod+close-square-bracket"
+                                             :fn      (fn [_] (js/window.history.forward))}
 
-   :search/re-index                {:binding "mod+c mod+s"
-                                    :fn      (fn [_] (search-handler/rebuild-indices! true))}
+   :search/re-index                         {:binding "mod+c mod+s"
+                                             :fn      (fn [_] (search-handler/rebuild-indices! true))}
 
-   :sidebar/open-today-page        {:binding (if mac? "mod+shift+j" "alt+shift+j")
-                                    :fn      page-handler/open-today-in-sidebar}
+   :sidebar/open-today-page                 {:binding (if mac? "mod+shift+j" "alt+shift+j")
+                                             :fn      page-handler/open-today-in-sidebar}
 
-   :sidebar/close-top              {:binding "c t"
-                                    :fn      #(state/sidebar-remove-block! 0)}
+   :sidebar/close-top                       {:binding "c t"
+                                             :fn      #(state/sidebar-remove-block! 0)}
 
-   :sidebar/clear                  {:binding "mod+c mod+c"
-                                    :fn      #(do
-                                                (state/clear-sidebar-blocks!)
-                                                (state/hide-right-sidebar!))}
+   :sidebar/clear                           {:binding "mod+c mod+c"
+                                             :fn      #(do
+                                                         (state/clear-sidebar-blocks!)
+                                                         (state/hide-right-sidebar!))}
 
-   :misc/copy                      {:binding "mod+c"
-                                    :fn      (fn [] (js/document.execCommand "copy"))}
+   :misc/copy                               {:binding "mod+c"
+                                             :fn      (fn [] (js/document.execCommand "copy"))}
 
-   :command-palette/toggle         {:binding "mod+shift+p"
-                                    :fn      #(do
-                                                (editor-handler/escape-editing)
-                                                (state/pub-event! [:modal/command-palette]))}
+   :command-palette/toggle                  {:binding "mod+shift+p"
+                                             :fn      #(do
+                                                         (editor-handler/escape-editing)
+                                                         (state/pub-event! [:modal/command-palette]))}
 
-   :graph/export-as-html           {:fn #(export-handler/download-repo-as-html!
-                                          (state/get-current-repo))
-                                    :binding false}
+   :graph/export-as-html                    {:fn      #(export-handler/download-repo-as-html!
+                                                         (state/get-current-repo))
+                                             :binding false}
 
-   :graph/open                     {:fn      #(do
-                                                (editor-handler/escape-editing)
-                                                (state/set-state! :ui/open-select :graph-open))
-                                    :binding "alt+shift+g"}
+   :graph/open                              {:fn      #(do
+                                                         (editor-handler/escape-editing)
+                                                         (state/set-state! :ui/open-select :graph-open))
+                                             :binding "alt+shift+g"}
 
-   :graph/remove                   {:fn      #(do
-                                                (editor-handler/escape-editing)
-                                                (state/set-state! :ui/open-select :graph-remove))
-                                    :binding false}
+   :graph/remove                            {:fn      #(do
+                                                         (editor-handler/escape-editing)
+                                                         (state/set-state! :ui/open-select :graph-remove))
+                                             :binding false}
 
-   :graph/add                      {:fn (fn [] (route-handler/redirect! {:to :repo-add}))
-                                    :binding false}
+   :graph/add                               {:fn      (fn [] (route-handler/redirect! {:to :repo-add}))
+                                             :binding false}
 
-   :graph/save                     {:fn #(state/pub-event! [:graph/save])
-                                    :binding false}
+   :graph/save                              {:fn      #(state/pub-event! [:graph/save])
+                                             :binding false}
 
-   :graph/re-index                 {:fn (fn []
-                                          (p/let [multiple-windows? (ipc/ipc "graphHasMultipleWindows" (state/get-current-repo))]
-                                            (state/pub-event! [:graph/ask-for-re-index (atom multiple-windows?) nil])))
-                                    :binding false}
+   :graph/re-index                          {:fn      (fn []
+                                                        (p/let [multiple-windows? (ipc/ipc "graphHasMultipleWindows" (state/get-current-repo))]
+                                                          (state/pub-event! [:graph/ask-for-re-index (atom multiple-windows?) nil])))
+                                             :binding false}
 
-   :command/run                    {:binding "mod+shift+1"
-                                    :inactive (not (util/electron?))
-                                    :fn      #(do
-                                                (editor-handler/escape-editing)
-                                                (state/pub-event! [:command/run]))}
+   :command/run                             {:binding  "mod+shift+1"
+                                             :inactive (not (util/electron?))
+                                             :fn       #(do
+                                                          (editor-handler/escape-editing)
+                                                          (state/pub-event! [:command/run]))}
 
-   :go/home                        {:binding "g h"
-                                    :fn      #(route-handler/redirect-to-home!)}
+   :go/home                                 {:binding "g h"
+                                             :fn      #(route-handler/redirect-to-home!)}
 
-   :go/all-pages                   {:binding "g a"
-                                    :fn      route-handler/redirect-to-all-pages!}
+   :go/all-pages                            {:binding "g a"
+                                             :fn      route-handler/redirect-to-all-pages!}
 
-   :go/graph-view                  {:binding "g g"
-                                    :fn      route-handler/redirect-to-graph-view!}
+   :go/graph-view                           {:binding "g g"
+                                             :fn      route-handler/redirect-to-graph-view!}
 
-   :go/all-graphs                  {:binding "g shift+g"
-                                    :fn      route-handler/redirect-to-all-graphs}
+   :go/all-graphs                           {:binding "g shift+g"
+                                             :fn      route-handler/redirect-to-all-graphs}
 
-   :go/whiteboards                  {:binding "g w"
-                                     :fn      route-handler/redirect-to-whiteboard-dashboard!}
+   :go/whiteboards                          {:binding "g w"
+                                             :fn      route-handler/redirect-to-whiteboard-dashboard!}
 
-   :go/keyboard-shortcuts          {:binding "g s"
-                                    :fn      #(state/pub-event! [:modal/keymap-manager])}
+   :go/keyboard-shortcuts                   {:binding "g s"
+                                             :fn      #(state/pub-event! [:modal/keymap])}
 
-   :go/tomorrow                    {:binding "g t"
-                                    :fn      journal-handler/go-to-tomorrow!}
+   :go/tomorrow                             {:binding "g t"
+                                             :fn      journal-handler/go-to-tomorrow!}
 
-   :go/next-journal                {:binding "g n"
-                                    :fn      journal-handler/go-to-next-journal!}
+   :go/next-journal                         {:binding "g n"
+                                             :fn      journal-handler/go-to-next-journal!}
 
-   :go/prev-journal                {:binding "g p"
-                                    :fn      journal-handler/go-to-prev-journal!}
+   :go/prev-journal                         {:binding "g p"
+                                             :fn      journal-handler/go-to-prev-journal!}
 
-   :go/flashcards                  {:binding "g f"
-                                    :fn      (fn []
-                                               (if (state/modal-opened?)
-                                                 (state/close-modal!)
-                                                 (state/pub-event! [:modal/show-cards])))}
+   :go/flashcards                           {:binding "g f"
+                                             :fn      (fn []
+                                                        (if (state/modal-opened?)
+                                                          (state/close-modal!)
+                                                          (state/pub-event! [:modal/show-cards])))}
 
-   :ui/toggle-document-mode        {:binding "t d"
-                                    :fn      state/toggle-document-mode!}
+   :ui/toggle-document-mode                 {:binding "t d"
+                                             :fn      state/toggle-document-mode!}
 
-   :ui/toggle-settings              {:binding (if mac? "t s" ["t s" "mod+,"])
-                                     :fn      ui-handler/toggle-settings-modal!}
+   :ui/toggle-settings                      {:binding (if mac? ["t s" "mod+,"] "t s")
+                                             :fn      ui-handler/toggle-settings-modal!}
 
-   :ui/toggle-right-sidebar         {:binding "t r"
-                                     :fn      ui-handler/toggle-right-sidebar!}
+   :ui/toggle-right-sidebar                 {:binding "t r"
+                                             :fn      ui-handler/toggle-right-sidebar!}
 
-   :ui/toggle-left-sidebar          {:binding "t l"
-                                     :fn      state/toggle-left-sidebar!}
+   :ui/toggle-left-sidebar                  {:binding "t l"
+                                             :fn      state/toggle-left-sidebar!}
 
-   :ui/toggle-help                  {:binding "shift+/"
-                                     :fn      ui-handler/toggle-help!}
+   :ui/toggle-help                          {:binding "shift+/"
+                                             :fn      ui-handler/toggle-help!}
 
-   :ui/toggle-theme                 {:binding "t t"
-                                     :fn      state/toggle-theme!}
+   :ui/toggle-theme                         {:binding "t t"
+                                             :fn      state/toggle-theme!}
 
-   :ui/toggle-contents              {:binding "alt+shift+c"
-                                     :fn      ui-handler/toggle-contents!}
+   :ui/toggle-contents                      {:binding "alt+shift+c"
+                                             :fn      ui-handler/toggle-contents!}
 
-   :command/toggle-favorite         {:binding "mod+shift+f"
-                                     :fn      page-handler/toggle-favorite!}
+   :command/toggle-favorite                 {:binding "mod+shift+f"
+                                             :fn      page-handler/toggle-favorite!}
 
-   :editor/open-file-in-default-app {:binding "mod+d mod+a"
-                                     :inactive (not (util/electron?))
-                                     :fn      page-handler/open-file-in-default-app}
+   :editor/open-file-in-default-app         {:binding  "mod+d mod+a"
+                                             :inactive (not (util/electron?))
+                                             :fn       page-handler/open-file-in-default-app}
 
-   :editor/open-file-in-directory   {:binding "mod+d mod+i"
-                                     :inactive (not (util/electron?))
-                                     :fn      page-handler/open-file-in-directory}
+   :editor/open-file-in-directory           {:binding  "mod+d mod+i"
+                                             :inactive (not (util/electron?))
+                                             :fn       page-handler/open-file-in-directory}
 
-   :editor/copy-current-file        {:binding false
-                                     :inactive (not (util/electron?))
-                                     :fn      page-handler/copy-current-file}
+   :editor/copy-current-file                {:binding  false
+                                             :inactive (not (util/electron?))
+                                             :fn       page-handler/copy-current-file}
 
-   :editor/copy-page-url            {:binding false
-                                     :inactive (not (util/electron?))
-                                     :fn      #(page-handler/copy-page-url)}
+   :editor/copy-page-url                    {:binding  false
+                                             :inactive (not (util/electron?))
+                                             :fn       #(page-handler/copy-page-url)}
 
-   :ui/toggle-wide-mode             {:binding "t w"
-                                     :fn      ui-handler/toggle-wide-mode!}
+   :ui/toggle-wide-mode                     {:binding "t w"
+                                             :fn      ui-handler/toggle-wide-mode!}
 
-   :ui/select-theme-color           {:binding "t i"
-                                     :fn      plugin-handler/show-themes-modal!}
+   :ui/select-theme-color                   {:binding "t i"
+                                             :fn      plugin-handler/show-themes-modal!}
 
-   :ui/goto-plugins                 {:binding "t p"
-                                     :inactive (not config/lsp-enabled?)
-                                     :fn      plugin-handler/goto-plugins-dashboard!}
+   :ui/goto-plugins                         {:binding  "t p"
+                                             :inactive (not config/lsp-enabled?)
+                                             :fn       plugin-handler/goto-plugins-dashboard!}
 
-   :ui/install-plugins-from-file    {:binding false
-                                     :inactive (not (config/plugin-config-enabled?))
-                                     :fn       plugin-config-handler/open-replace-plugins-modal}
+   :ui/install-plugins-from-file            {:binding  false
+                                             :inactive (not (config/plugin-config-enabled?))
+                                             :fn       plugin-config-handler/open-replace-plugins-modal}
 
-   :ui/clear-all-notifications      {:binding false
-                                     :fn      :frontend.handler.notification/clear-all!}
+   :ui/clear-all-notifications              {:binding false
+                                             :fn      :frontend.handler.notification/clear-all!}
 
-   :editor/toggle-open-blocks       {:binding "t o"
-                                     :fn      editor-handler/toggle-open!}
+   :editor/toggle-open-blocks               {:binding "t o"
+                                             :fn      editor-handler/toggle-open!}
 
-   :ui/toggle-cards                 {:binding "t c"
-                                     :fn      ui-handler/toggle-cards!}
+   :ui/toggle-cards                         {:binding "t c"
+                                             :fn      ui-handler/toggle-cards!}
 
-   :git/commit                      {:binding "mod+g c"
-                                     :inactive (not (util/electron?))
-                                     :fn      commit/show-commit-modal!}
+   :git/commit                              {:binding  "mod+g c"
+                                             :inactive (not (util/electron?))
+                                             :fn       commit/show-commit-modal!}
 
-   :dev/show-block-data            {:binding false
-                                    :inactive (not (state/developer-mode?))
-                                    :fn :frontend.handler.common.developer/show-block-data}
+   :dev/show-block-data                     {:binding  false
+                                             :inactive (not (state/developer-mode?))
+                                             :fn       :frontend.handler.common.developer/show-block-data}
 
-   :dev/show-block-ast             {:binding false
-                                    :inactive (not (state/developer-mode?))
-                                    :fn :frontend.handler.common.developer/show-block-ast}
+   :dev/show-block-ast                      {:binding  false
+                                             :inactive (not (state/developer-mode?))
+                                             :fn       :frontend.handler.common.developer/show-block-ast}
 
-   :dev/show-page-data             {:binding false
-                                    :inactive (not (state/developer-mode?))
-                                    :fn :frontend.handler.common.developer/show-page-data}
+   :dev/show-page-data                      {:binding  false
+                                             :inactive (not (state/developer-mode?))
+                                             :fn       :frontend.handler.common.developer/show-page-data}
 
-   :dev/show-page-ast              {:binding false
-                                    :inactive (not (state/developer-mode?))
-                                    :fn :frontend.handler.common.developer/show-page-ast}})
+   :dev/show-page-ast                       {:binding  false
+                                             :inactive (not (state/developer-mode?))
+                                             :fn       :frontend.handler.common.developer/show-page-ast}})
 
 (let [keyboard-commands
-      {::commands (set (keys all-default-keyboard-shortcuts))
+      {::commands       (set (keys all-built-in-keyboard-shortcuts))
        ::dicts/commands dicts/abbreviated-commands}]
   (assert (= (::commands keyboard-commands) (::dicts/commands keyboard-commands))
           (str "Keyboard commands must have an english label"
@@ -557,7 +558,16 @@
       (throw (ex-info (str "Unable to resolve " keyword-fn " to a fn") {})))))
 
 (defn build-category-map [ks]
-  (->> (select-keys all-default-keyboard-shortcuts ks)
+  (->> (if (sequential? ks)
+         ks (let [{:keys [ns includes excludes]} ks]
+              (->> (keys all-built-in-keyboard-shortcuts)
+                   (filter (fn [k]
+                             (and (or (and ns (keyword? k)
+                                           (contains? (->> (if (seqable? ns) (seq ns) [ns]) (map #(name %)) (set))
+                                                      (namespace k)))
+                                      (and includes (contains? (set includes) k)))
+                                  (if (not (seq excludes)) true (not (contains? (set excludes) k)))))))))
+       (select-keys all-built-in-keyboard-shortcuts)
        (remove (comp :inactive val))
        ;; Convert keyword fns to real fns
        (map (fn [[k v]]
@@ -567,392 +577,359 @@
        (into {})))
 
 ;; This is the only var that should be publicly expose :fn functionality
-(defonce ^:large-vars/data-var config
+(defonce ^:large-vars/data-var *config
   (atom
    {:shortcut.handler/date-picker
-    (build-category-map [:date-picker/complete
-                         :date-picker/prev-day
-                         :date-picker/next-day
-                         :date-picker/prev-week
-                         :date-picker/next-week])
+    (build-category-map {:ns :date-picker})
 
     :shortcut.handler/pdf
-    (-> (build-category-map [:pdf/previous-page
-                             :pdf/next-page
-                             :pdf/close
-                             :pdf/find])
+    (-> (build-category-map {:ns :pdf})
         (with-meta {:before m/enable-when-not-editing-mode!}))
 
     :shortcut.handler/whiteboard
-    (-> (build-category-map [:whiteboard/select
-                             :whiteboard/pan
-                             :whiteboard/portal
-                             :whiteboard/pencil
-                             :whiteboard/highlighter
-                             :whiteboard/eraser
-                             :whiteboard/connector
-                             :whiteboard/text
-                             :whiteboard/rectangle
-                             :whiteboard/ellipse
-                             :whiteboard/reset-zoom
-                             :whiteboard/zoom-to-fit
-                             :whiteboard/zoom-to-selection
-                             :whiteboard/zoom-out
-                             :whiteboard/zoom-in
-                             :whiteboard/send-backward
-                             :whiteboard/send-to-back
-                             :whiteboard/bring-forward
-                             :whiteboard/bring-to-front
-                             :whiteboard/lock
-                             :whiteboard/unlock
-                             :whiteboard/group
-                             :whiteboard/ungroup
-                             :whiteboard/toggle-grid])
+    (-> (build-category-map {:ns :whiteboard})
         (with-meta {:before m/enable-when-not-editing-mode!}))
 
     :shortcut.handler/auto-complete
-    (build-category-map [:auto-complete/complete
-                         :auto-complete/prev
-                         :auto-complete/next
-                         :auto-complete/shift-complete
-                         :auto-complete/open-link])
+    (build-category-map {:ns :auto-complete})
 
     :shortcut.handler/cards
-    (-> (build-category-map [:cards/toggle-answers
-                             :cards/next-card
-                             :cards/forgotten
-                             :cards/remembered
-                             :cards/recall])
+    (-> (build-category-map {:ns :cards})
         (with-meta {:before m/enable-when-not-editing-mode!}))
 
     :shortcut.handler/block-editing-only
-    (->
-     (build-category-map [:editor/escape-editing
-                          :editor/backspace
-                          :editor/delete
-                          :editor/new-block
-                          :editor/new-line
-                          :editor/follow-link
-                          :editor/open-link-in-sidebar
-                          :editor/bold
-                          :editor/italics
-                          :editor/highlight
-                          :editor/strike-through
-                          :editor/clear-block
-                          :editor/kill-line-before
-                          :editor/kill-line-after
-                          :editor/beginning-of-block
-                          :editor/end-of-block
-                          :editor/forward-word
-                          :editor/backward-word
-                          :editor/forward-kill-word
-                          :editor/backward-kill-word
-                          :editor/replace-block-reference-at-point
-                          :editor/copy-embed
-                          :editor/paste-text-in-one-block-at-point
-                          :editor/insert-youtube-timestamp])
-     (with-meta {:before m/enable-when-editing-mode!}))
+    (-> (build-category-map
+          [:editor/escape-editing
+           :editor/backspace
+           :editor/delete
+           :editor/zoom-in
+           :editor/zoom-out
+           :editor/new-block
+           :editor/new-line
+           :editor/follow-link
+           :editor/open-link-in-sidebar
+           :editor/bold
+           :editor/italics
+           :editor/highlight
+           :editor/strike-through
+           :editor/clear-block
+           :editor/kill-line-before
+           :editor/kill-line-after
+           :editor/beginning-of-block
+           :editor/end-of-block
+           :editor/forward-word
+           :editor/backward-word
+           :editor/forward-kill-word
+           :editor/backward-kill-word
+           :editor/replace-block-reference-at-point
+           :editor/copy-embed
+           :editor/paste-text-in-one-block-at-point
+           :editor/insert-youtube-timestamp])
+        (with-meta {:before m/enable-when-editing-mode!}))
 
     :shortcut.handler/editor-global
-    (->
-     (build-category-map [:graph/export-as-html
-                          :graph/open
-                          :graph/remove
-                          :graph/add
-                          :graph/save
-                          :graph/re-index
-                          :editor/cycle-todo
-                          :editor/up
-                          :editor/down
-                          :editor/left
-                          :editor/right
-                          :editor/select-up
-                          :editor/select-down
-                          :editor/move-block-up
-                          :editor/move-block-down
-                          :editor/open-edit
-                          :editor/select-block-up
-                          :editor/select-block-down
-                          :editor/select-parent
-                          :editor/delete-selection
-                          :editor/expand-block-children
-                          :editor/collapse-block-children
-                          :editor/indent
-                          :editor/outdent
-                          :editor/copy
-                          :editor/copy-text
-                          :editor/cut
-                          :command/toggle-favorite])
-     (with-meta {:before m/enable-when-not-component-editing!}))
+    (-> (build-category-map
+          [:graph/export-as-html
+           :graph/open
+           :graph/remove
+           :graph/add
+           :graph/save
+           :graph/re-index
+           :editor/cycle-todo
+           :editor/up
+           :editor/down
+           :editor/left
+           :editor/right
+           :editor/select-up
+           :editor/select-down
+           :editor/move-block-up
+           :editor/move-block-down
+           :editor/open-edit
+           :editor/select-block-up
+           :editor/select-block-down
+           :editor/select-parent
+           :editor/delete-selection
+           :editor/expand-block-children
+           :editor/collapse-block-children
+           :editor/indent
+           :editor/outdent
+           :editor/copy
+           :editor/copy-text
+           :editor/cut
+           :command/toggle-favorite])
+        (with-meta {:before m/enable-when-not-component-editing!}))
 
     :shortcut.handler/global-prevent-default
-    (->
-     (build-category-map [:editor/insert-link
-                          :editor/select-all-blocks
-                          :editor/zoom-in
-                          :editor/zoom-out
-                          :editor/toggle-undo-redo-mode
-                          :editor/toggle-number-list
-                          :editor/undo
-                          :editor/redo
-                          :ui/toggle-brackets
-                          :go/search-in-page
-                          :go/search
-                          :go/electron-find-in-page
-                          :go/electron-jump-to-the-next
-                          :go/electron-jump-to-the-previous
-                          :go/backward
-                          :go/forward
-                          :search/re-index
-                          :sidebar/open-today-page
-                          :sidebar/clear
-                          :command/run
-                          :command-palette/toggle])
-     (with-meta {:before m/prevent-default-behavior}))
+    (-> (build-category-map
+          [:editor/insert-link
+           :editor/select-all-blocks
+           :editor/toggle-undo-redo-mode
+           :editor/toggle-number-list
+           :editor/undo
+           :editor/redo
+           :ui/toggle-brackets
+           :go/search-in-page
+           :go/search
+           :go/electron-find-in-page
+           :go/electron-jump-to-the-next
+           :go/electron-jump-to-the-previous
+           :go/backward
+           :go/forward
+           :search/re-index
+           :sidebar/open-today-page
+           :sidebar/clear
+           :command/run
+           :command-palette/toggle])
+        (with-meta {:before m/prevent-default-behavior}))
+
+    :shortcut.handler/global-non-editing-only
+    (-> (build-category-map
+          [:go/home
+           :go/journals
+           :go/all-pages
+           :go/flashcards
+           :go/graph-view
+           :go/all-graphs
+           :go/whiteboards
+           :go/keyboard-shortcuts
+           :go/tomorrow
+           :go/next-journal
+           :go/prev-journal
+           :ui/toggle-document-mode
+           :ui/toggle-settings
+           :ui/toggle-right-sidebar
+           :ui/toggle-left-sidebar
+           :ui/toggle-help
+           :ui/toggle-theme
+           :ui/toggle-contents
+           :editor/open-file-in-default-app
+           :editor/open-file-in-directory
+           :editor/copy-current-file
+           :editor/copy-page-url
+           :editor/new-whiteboard
+           :ui/toggle-wide-mode
+           :ui/select-theme-color
+           :ui/goto-plugins
+           :ui/install-plugins-from-file
+           :editor/toggle-open-blocks
+           :ui/toggle-cards
+           :ui/clear-all-notifications
+           :git/commit
+           :sidebar/close-top
+           :dev/show-block-data
+           :dev/show-block-ast
+           :dev/show-page-data
+           :dev/show-page-ast])
+        (with-meta {:before m/enable-when-not-editing-mode!}))
 
     :shortcut.handler/misc
     ;; always overrides the copy due to "mod+c mod+s"
-    {:misc/copy              (:misc/copy              all-default-keyboard-shortcuts)}
-
-    :shortcut.handler/global-non-editing-only
-    (->
-     (build-category-map [:go/home
-                          :go/journals
-                          :go/all-pages
-                          :go/flashcards
-                          :go/graph-view
-                          :go/all-graphs
-                          :go/whiteboards
-                          :go/keyboard-shortcuts
-                          :go/tomorrow
-                          :go/next-journal
-                          :go/prev-journal
-                          :ui/toggle-document-mode
-                          :ui/toggle-settings
-                          :ui/toggle-right-sidebar
-                          :ui/toggle-left-sidebar
-                          :ui/toggle-help
-                          :ui/toggle-theme
-                          :ui/toggle-contents
-                          :editor/open-file-in-default-app
-                          :editor/open-file-in-directory
-                          :editor/copy-current-file
-                          :editor/copy-page-url
-                          :editor/new-whiteboard
-                          :ui/toggle-wide-mode
-                          :ui/select-theme-color
-                          :ui/goto-plugins
-                          :ui/install-plugins-from-file
-                          :editor/toggle-open-blocks
-                          :ui/toggle-cards
-                          :ui/clear-all-notifications
-                          :git/commit
-                          :sidebar/close-top
-                          :dev/show-block-data
-                          :dev/show-block-ast
-                          :dev/show-page-data
-                          :dev/show-page-ast])
-     (with-meta {:before m/enable-when-not-editing-mode!}))}))
+    {:misc/copy (:misc/copy all-built-in-keyboard-shortcuts)}}))
 
 ;; To add a new entry to this map, first add it here and then
 ;; a description for it in frontend.dicts.en/dicts
-(def ^:large-vars/data-var category*
-  "Full list of categories for docs purpose"
-  {:shortcut.category/basics
-   [:editor/new-block
-    :editor/new-line
-    :editor/indent
-    :editor/outdent
-    :editor/select-all-blocks
-    :editor/select-parent
-    :go/search
-    :go/search-in-page
-    :go/electron-find-in-page
-    :go/electron-jump-to-the-next
-    :go/electron-jump-to-the-previous
-    :editor/undo
-    :editor/redo
-    :editor/copy
-    :editor/copy-text
-    :editor/cut]
-
-   :shortcut.category/formatting
-   [:editor/bold
-    :editor/insert-link
-    :editor/italics
-    :editor/strike-through
-    :editor/highlight]
-
-   :shortcut.category/navigating
-   [:editor/up
-    :editor/down
-    :editor/left
-    :editor/right
-    :editor/zoom-in
-    :editor/zoom-out
-    :editor/collapse-block-children
-    :editor/expand-block-children
-    :editor/toggle-open-blocks
-    :go/backward
-    :go/forward
-    :go/home
-    :go/journals
-    :go/all-pages
-    :go/graph-view
-    :go/all-graphs
-    :go/whiteboards
-    :go/flashcards
-    :go/tomorrow
-    :go/next-journal
-    :go/prev-journal
-    :go/keyboard-shortcuts]
-
-   :shortcut.category/block-editing
-   [:editor/backspace
-    :editor/delete
-    :editor/indent
-    :editor/outdent
-    :editor/new-block
-    :editor/new-line
-    :editor/zoom-in
-    :editor/zoom-out
-    :editor/cycle-todo
-    :editor/follow-link
-    :editor/open-link-in-sidebar
-    :editor/move-block-up
-    :editor/move-block-down
-    :editor/escape-editing]
-
-   :shortcut.category/block-command-editing
-   [:editor/backspace
-    :editor/clear-block
-    :editor/kill-line-before
-    :editor/kill-line-after
-    :editor/beginning-of-block
-    :editor/end-of-block
-    :editor/forward-word
-    :editor/backward-word
-    :editor/forward-kill-word
-    :editor/backward-kill-word
-    :editor/replace-block-reference-at-point
-    :editor/copy-embed
-    :editor/paste-text-in-one-block-at-point
-    :editor/select-up
-    :editor/select-down]
-
-   :shortcut.category/block-selection
-   [:editor/open-edit
-    :editor/select-all-blocks
-    :editor/select-parent
-    :editor/select-block-up
-    :editor/select-block-down
-    :editor/delete-selection]
-
-   :shortcut.category/toggle
-   [:ui/toggle-help
-    :editor/toggle-open-blocks
-    :editor/toggle-undo-redo-mode
-    :editor/toggle-number-list
-    :ui/toggle-wide-mode
-    :ui/toggle-cards
-    :ui/toggle-document-mode
-    :ui/toggle-brackets
-    :ui/toggle-theme
-    :ui/toggle-left-sidebar
-    :ui/toggle-right-sidebar
-    :ui/toggle-settings
-    :ui/toggle-contents]
-
-   :shortcut.category/whiteboard
-   [:editor/new-whiteboard
-    :whiteboard/select
-    :whiteboard/pan
-    :whiteboard/portal
-    :whiteboard/pencil
-    :whiteboard/highlighter
-    :whiteboard/eraser
-    :whiteboard/connector
-    :whiteboard/text
-    :whiteboard/rectangle
-    :whiteboard/ellipse
-    :whiteboard/reset-zoom
-    :whiteboard/zoom-to-fit
-    :whiteboard/zoom-to-selection
-    :whiteboard/zoom-out
-    :whiteboard/zoom-in
-    :whiteboard/send-backward
-    :whiteboard/send-to-back
-    :whiteboard/bring-forward
-    :whiteboard/bring-to-front
-    :whiteboard/lock
-    :whiteboard/unlock
-    :whiteboard/group
-    :whiteboard/ungroup
-    :whiteboard/toggle-grid]
-   
-   :shortcut.category/others
-   [:pdf/previous-page
-    :pdf/next-page
-    :pdf/close
-    :pdf/find
-    :command/toggle-favorite
-    :command/run
-    :command-palette/toggle
-    :graph/export-as-html
-    :graph/open
-    :graph/remove
-    :graph/add
-    :graph/save
-    :graph/re-index
-    :sidebar/close-top
-    :sidebar/clear
-    :sidebar/open-today-page
-    :search/re-index
-    :editor/insert-youtube-timestamp
-    :editor/open-file-in-default-app
-    :editor/open-file-in-directory
-    :editor/copy-page-url
-    :auto-complete/prev
-    :auto-complete/next
-    :auto-complete/complete
-    :auto-complete/shift-complete
-    :auto-complete/open-link
-    :date-picker/prev-day
-    :date-picker/next-day
-    :date-picker/prev-week
-    :date-picker/next-week
-    :date-picker/complete
-    :git/commit
-    :dev/show-block-data
-    :dev/show-block-ast
-    :dev/show-page-data
-    :dev/show-page-ast
-    :ui/clear-all-notifications]
-
-   :shortcut.category/plugins
-   []})
-
-(let [category-maps {::category (set (keys category*))
+;; Full list of categories for docs purpose
+(defonce ^:large-vars/data-var *category
+  (atom
+   {:shortcut.category/basics
+    [:editor/new-block
+     :editor/new-line
+     :editor/indent
+     :editor/outdent
+     :editor/select-all-blocks
+     :editor/select-parent
+     :go/search
+     :go/search-in-page
+     :go/electron-find-in-page
+     :go/electron-jump-to-the-next
+     :go/electron-jump-to-the-previous
+     :editor/undo
+     :editor/redo
+     :editor/copy
+     :editor/copy-text
+     :editor/cut]
+
+    :shortcut.category/formatting
+    [:editor/bold
+     :editor/insert-link
+     :editor/italics
+     :editor/strike-through
+     :editor/highlight]
+
+    :shortcut.category/navigating
+    [:editor/up
+     :editor/down
+     :editor/left
+     :editor/right
+     :editor/collapse-block-children
+     :editor/expand-block-children
+     :editor/toggle-open-blocks
+     :go/backward
+     :go/forward
+     :go/home
+     :go/journals
+     :go/all-pages
+     :go/graph-view
+     :go/all-graphs
+     :go/whiteboards
+     :go/flashcards
+     :go/tomorrow
+     :go/next-journal
+     :go/prev-journal
+     :go/keyboard-shortcuts]
+
+    :shortcut.category/block-editing
+    [:editor/backspace
+     :editor/delete
+     :editor/indent
+     :editor/outdent
+     :editor/new-block
+     :editor/new-line
+     :editor/zoom-in
+     :editor/zoom-out
+     :editor/cycle-todo
+     :editor/follow-link
+     :editor/open-link-in-sidebar
+     :editor/move-block-up
+     :editor/move-block-down
+     :editor/escape-editing]
+
+    :shortcut.category/block-command-editing
+    [:editor/backspace
+     :editor/clear-block
+     :editor/kill-line-before
+     :editor/kill-line-after
+     :editor/beginning-of-block
+     :editor/end-of-block
+     :editor/forward-word
+     :editor/backward-word
+     :editor/forward-kill-word
+     :editor/backward-kill-word
+     :editor/replace-block-reference-at-point
+     :editor/copy-embed
+     :editor/paste-text-in-one-block-at-point
+     :editor/select-up
+     :editor/select-down]
+
+    :shortcut.category/block-selection
+    [:editor/open-edit
+     :editor/select-all-blocks
+     :editor/select-parent
+     :editor/select-block-up
+     :editor/select-block-down
+     :editor/delete-selection]
+
+    :shortcut.category/toggle
+    [:ui/toggle-help
+     :editor/toggle-open-blocks
+     :editor/toggle-undo-redo-mode
+     :editor/toggle-number-list
+     :ui/toggle-wide-mode
+     :ui/toggle-cards
+     :ui/toggle-document-mode
+     :ui/toggle-brackets
+     :ui/toggle-theme
+     :ui/toggle-left-sidebar
+     :ui/toggle-right-sidebar
+     :ui/toggle-settings
+     :ui/toggle-contents]
+
+    :shortcut.category/whiteboard
+    [:editor/new-whiteboard
+     :whiteboard/select
+     :whiteboard/pan
+     :whiteboard/portal
+     :whiteboard/pencil
+     :whiteboard/highlighter
+     :whiteboard/eraser
+     :whiteboard/connector
+     :whiteboard/text
+     :whiteboard/rectangle
+     :whiteboard/ellipse
+     :whiteboard/reset-zoom
+     :whiteboard/zoom-to-fit
+     :whiteboard/zoom-to-selection
+     :whiteboard/zoom-out
+     :whiteboard/zoom-in
+     :whiteboard/send-backward
+     :whiteboard/send-to-back
+     :whiteboard/bring-forward
+     :whiteboard/bring-to-front
+     :whiteboard/lock
+     :whiteboard/unlock
+     :whiteboard/group
+     :whiteboard/ungroup
+     :whiteboard/toggle-grid]
+
+    :shortcut.category/others
+    [:pdf/previous-page
+     :pdf/next-page
+     :pdf/close
+     :pdf/find
+     :command/toggle-favorite
+     :command/run
+     :command-palette/toggle
+     :graph/export-as-html
+     :graph/open
+     :graph/remove
+     :graph/add
+     :graph/save
+     :graph/re-index
+     :sidebar/close-top
+     :sidebar/clear
+     :sidebar/open-today-page
+     :search/re-index
+     :editor/insert-youtube-timestamp
+     :editor/open-file-in-default-app
+     :editor/open-file-in-directory
+     :editor/copy-page-url
+     :auto-complete/prev
+     :auto-complete/next
+     :auto-complete/complete
+     :auto-complete/shift-complete
+     :auto-complete/open-link
+     :date-picker/prev-day
+     :date-picker/next-day
+     :date-picker/prev-week
+     :date-picker/next-week
+     :date-picker/complete
+     :git/commit
+     :dev/show-block-data
+     :dev/show-block-ast
+     :dev/show-page-data
+     :dev/show-page-ast
+     :ui/clear-all-notifications]
+
+    :shortcut.category/plugins
+    []}))
+
+(let [category-maps {::category       (set (keys @*category))
                      ::dicts/category dicts/categories}]
   (assert (= (::category category-maps) (::dicts/category category-maps))
           (str "Keys for category maps must have an english label "
                (data/diff (::category category-maps) (::dicts/category category-maps)))))
 
-(def category
+(defn get-category-shortcuts
   "Active list of categories for docs purpose"
-  (update-vals
-   category*
-   (fn [v]
-     (vec (remove #(:inactive (get all-default-keyboard-shortcuts %)) v)))))
+  [name]
+  (get @*category name))
 
 (def *shortcut-cmds (atom {}))
 
 (defn add-shortcut!
   [handler-id id shortcut-map]
-  (swap! config assoc-in [handler-id id] shortcut-map)
-  (swap! *shortcut-cmds assoc id (:cmd shortcut-map)))
+  (swap! *config assoc-in [handler-id id] shortcut-map)
+  (swap! *shortcut-cmds assoc id (:cmd shortcut-map))
+  (let [plugin? (str/starts-with? (str id) ":plugin.")
+        category (or (:category shortcut-map)
+                     (if plugin?
+                       :shortcut.category/plugins
+                       :shortcut.category/others))]
+    (swap! *category update category #(conj % id))))
 
 (defn remove-shortcut!
   [handler-id id]
-  (swap! config medley/dissoc-in [handler-id id])
-  (swap! *shortcut-cmds dissoc id))
+  (swap! *config medley/dissoc-in [handler-id id])
+  (swap! *shortcut-cmds dissoc id)
+  (doseq [category (keys @*category)]
+    (swap! *category update category (fn [ids] (remove #(= % id) ids)))))

+ 158 - 74
src/main/frontend/modules/shortcut/core.cljs

@@ -1,9 +1,12 @@
 (ns frontend.modules.shortcut.core
   (:require [clojure.string :as str]
             [frontend.handler.config :as config-handler]
+            [frontend.handler.global-config :as global-config-handler]
+            [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.notification :as notification]
             [frontend.modules.shortcut.data-helper :as dh]
             [frontend.modules.shortcut.config :as shortcut-config]
+            [frontend.modules.shortcut.utils :as shortcut-utils]
             [frontend.state :as state]
             [frontend.util :as util]
             [goog.events :as events]
@@ -13,15 +16,15 @@
   (:import [goog.events KeyCodes KeyHandler KeyNames]
            [goog.ui KeyboardShortcutHandler]))
 
-(def *installed (atom {}))
-(def *inited? (atom false))
-(def *pending (atom []))
+(defonce *installed-handlers (atom {}))
+(defonce *pending-inited? (atom false))
+(defonce *pending-shortcuts (atom []))
 
 (def global-keys #js
-                  [KeyCodes/TAB
-                   KeyCodes/ENTER
-                   KeyCodes/BACKSPACE KeyCodes/DELETE
-                   KeyCodes/UP KeyCodes/LEFT KeyCodes/DOWN KeyCodes/RIGHT])
+        [KeyCodes/TAB
+         KeyCodes/ENTER
+         KeyCodes/BACKSPACE KeyCodes/DELETE
+         KeyCodes/UP KeyCodes/LEFT KeyCodes/DOWN KeyCodes/RIGHT])
 
 (def key-names (js->clj KeyNames))
 
@@ -29,16 +32,25 @@
 
 (defn consume-pending-shortcuts!
   []
-  (when (and @*inited? (seq @*pending))
-    (doseq [[handler-id id shortcut] @*pending]
+  (when (and @*pending-inited? (seq @*pending-shortcuts))
+    (doseq [[handler-id id shortcut] @*pending-shortcuts]
       (register-shortcut! handler-id id shortcut))
-    (reset! *pending [])))
+    (reset! *pending-shortcuts [])))
 
 (defn- get-handler-by-id
   [handler-id]
-  (-> (filter #(= (:group %) handler-id) (vals @*installed))
-      first
-      :handler))
+  (->> (vals @*installed-handlers)
+       (filter #(= (:group %) handler-id))
+       first
+       :handler))
+
+(defn- get-installed-ids-by-handler-id
+  [handler-id]
+  (some->> @*installed-handlers
+           (filter #(= (:group (second %)) handler-id))
+           (map first)
+           (remove nil?)
+           (vec)))
 
 (defn register-shortcut!
   "Register a shortcut, notice the id need to be a namespaced keyword to avoid
@@ -50,14 +62,14 @@
   ([handler-id id]
    (register-shortcut! handler-id id nil))
   ([handler-id id shortcut-map]
-   (if (and (keyword? handler-id) (not @*inited?))
-     (swap! *pending conj [handler-id id shortcut-map])
-     (when-let [handler (if (or (string? handler-id) (keyword? handler-id))
-                          (let [handler-id (keyword handler-id)]
-                            (get-handler-by-id handler-id))
+   (if (and (keyword? handler-id) (not @*pending-inited?))
+     (swap! *pending-shortcuts conj [handler-id id shortcut-map])
+     (when-let [^js handler (if (or (string? handler-id) (keyword? handler-id))
+                              (let [handler-id (keyword handler-id)]
+                                (get-handler-by-id handler-id))
 
-                          ;; handler
-                          handler-id)]
+                              ;; as Handler instance
+                              handler-id)]
 
        (when shortcut-map
          (shortcut-config/add-shortcut! handler-id id shortcut-map))
@@ -66,7 +78,7 @@
          (doseq [k (dh/shortcut-binding id)]
            (try
              (log/debug :shortcut/register-shortcut {:id id :binding k})
-             (.registerShortcut handler (util/keyname id) (util/normalize-user-keyname k))
+             (.registerShortcut handler (util/keyname id) (shortcut-utils/undecorate-binding k))
              (catch :default e
                (log/error :shortcut/register-shortcut {:id      id
                                                        :binding k
@@ -81,15 +93,17 @@
   (when-let [handler (get-handler-by-id handler-id)]
     (when-let [ks (dh/shortcut-binding shortcut-id)]
       (doseq [k ks]
-        (.unregisterShortcut ^js handler (util/normalize-user-keyname k))))
+        (.unregisterShortcut ^js handler (shortcut-utils/undecorate-binding k))))
     (shortcut-config/remove-shortcut! handler-id shortcut-id)))
 
 (defn uninstall-shortcut-handler!
-  [install-id]
-  (when-let [handler (-> (get @*installed install-id)
-                         :handler)]
-    (.dispose ^js handler)
-    (swap! *installed dissoc install-id)))
+  ([install-id] (uninstall-shortcut-handler! install-id false))
+  ([install-id refresh?]
+   (when-let [handler (-> (get @*installed-handlers install-id)
+                          :handler)]
+     (.dispose ^js handler)
+     (js/console.debug "[shortcuts]" "uninstall handler" (-> @*installed-handlers (get install-id) :group str) (if refresh? "*" ""))
+     (swap! *installed-handlers dissoc install-id))))
 
 (defn install-shortcut-handler!
   [handler-id {:keys [set-global-keys?
@@ -97,11 +111,15 @@
                       state]
                :or   {set-global-keys? true
                       prevent-default? false}}]
-  (when-let [install-id (get-handler-by-id handler-id)]
-    (uninstall-shortcut-handler! install-id))
+
+  ;; force uninstall existed handler
+  (some->>
+    (get-installed-ids-by-handler-id handler-id)
+    (map #(uninstall-shortcut-handler! % true))
+    (doall))
 
   (let [shortcut-map (dh/shortcut-map handler-id state)
-        handler      (new KeyboardShortcutHandler js/window)]
+        handler (new KeyboardShortcutHandler js/window)]
     ;; set arrows enter, tab to global
     (when set-global-keys?
       (.setGlobalKeys handler global-keys))
@@ -114,66 +132,109 @@
       (register-shortcut! handler id))
 
     (let [f (fn [e]
-              (let [shortcut-map (dh/shortcut-map handler-id state)
-                    dispatch-fn (get shortcut-map (keyword (.-identifier e)))]
+              (let [id (keyword (.-identifier e))
+                    shortcut-map (dh/shortcut-map handler-id state) ;; required to get shortcut map dynamically
+                    dispatch-fn (get shortcut-map id)]
                 ;; trigger fn
-                (when dispatch-fn (dispatch-fn e))))
+                (when dispatch-fn
+                  (plugin-handler/hook-lifecycle-fn! id dispatch-fn e))))
           install-id (random-uuid)
-          data       {install-id
-                      {:group      handler-id
-                       :dispatch-fn f
-                       :handler    handler}}]
+          data {install-id
+                {:group       handler-id
+                 :dispatch-fn f
+                 :handler     handler}}]
 
       (.listen handler EventType/SHORTCUT_TRIGGERED f)
 
-      (swap! *installed merge data)
+      (js/console.debug "[shortcuts] install handler" (str handler-id))
+      (swap! *installed-handlers merge data)
 
       install-id)))
 
 (defn- install-shortcuts!
-  []
-  (->> [:shortcut.handler/misc
-        :shortcut.handler/editor-global
-        :shortcut.handler/global-non-editing-only
-        :shortcut.handler/global-prevent-default]
+  [handler-ids]
+  (->> (or (seq handler-ids)
+           [:shortcut.handler/misc
+            :shortcut.handler/editor-global
+            :shortcut.handler/global-non-editing-only
+            :shortcut.handler/global-prevent-default])
        (map #(install-shortcut-handler! % {}))
        doall))
 
-(defn mixin [handler-id]
+(defn mixin
+  ([handler-id] (mixin handler-id true))
+  ([handler-id remount-reinstall?]
+   (cond->
+     {:did-mount
+      (fn [state]
+        (let [install-id (install-shortcut-handler! handler-id {:state state})]
+          (assoc state ::install-id install-id)))
+
+      :will-unmount
+      (fn [state]
+        (when-let [install-id (::install-id state)]
+          (uninstall-shortcut-handler! install-id))
+        state)}
+
+     remount-reinstall?
+     (assoc
+       :will-remount
+       (fn [old-state new-state]
+         (util/profile "[shortcuts] reinstalled:"
+           (uninstall-shortcut-handler! (::install-id old-state))
+           (when-let [install-id (install-shortcut-handler! handler-id {:state new-state})]
+             (assoc new-state ::install-id install-id))))))))
+
+(defn mixin*
+  "This is an optimized version compared to (mixin).
+   And the shortcuts will not be frequently loaded and unloaded.
+   As well as ensuring unnecessary updates of components."
+  [handler-id]
   {:did-mount
    (fn [state]
-     (let [install-id (install-shortcut-handler! handler-id {:state state})]
-       (assoc state ::install-id install-id)))
+     (let [*state (volatile! state)
+           install-id (install-shortcut-handler! handler-id {:state *state})]
+       (assoc state ::install-id install-id
+                    ::*state *state)))
+
+   :will-remount
+   (fn [old-state new-state]
+     (when-let [*state (::*state old-state)]
+       (vreset! *state new-state))
+     new-state)
 
-   :will-remount (fn [old-state new-state]
-                  (uninstall-shortcut-handler! (::install-id old-state))
-                  (when-let [install-id (install-shortcut-handler! handler-id {:state new-state})]
-                    (assoc new-state ::install-id install-id)))
    :will-unmount
    (fn [state]
      (when-let [install-id (::install-id state)]
-       (uninstall-shortcut-handler! install-id))
+       (uninstall-shortcut-handler! install-id)
+       (some-> (::*state state) (vreset! nil)))
      state)})
 
-(defn unlisten-all []
-  (doseq [{:keys [handler group]} (vals @*installed)
-          :when (not= group :shortcut.handler/misc)]
-    (.removeAllListeners handler)))
-
-(defn listen-all []
-  (doseq [{:keys [handler group dispatch-fn]} (vals @*installed)
+(defn unlisten-all!
+  ([] (unlisten-all! false))
+  ([dispose?]
+   (doseq [{:keys [handler group dispatch-fn]} (vals @*installed-handlers)
+           :when (not= group :shortcut.handler/misc)]
+     (if dispose?
+       (.dispose handler)
+       (events/unlisten handler EventType/SHORTCUT_TRIGGERED dispatch-fn)))))
+
+(defn listen-all! []
+  (doseq [{:keys [handler group dispatch-fn]} (vals @*installed-handlers)
           :when (not= group :shortcut.handler/misc)]
-    (events/listen handler EventType/SHORTCUT_TRIGGERED dispatch-fn)))
+    (if (.isDisposed handler)
+      (install-shortcut-handler! group {})
+      (events/listen handler EventType/SHORTCUT_TRIGGERED dispatch-fn))))
 
 (def disable-all-shortcuts
   {:will-mount
    (fn [state]
-     (unlisten-all)
+     (unlisten-all!)
      state)
 
    :will-unmount
    (fn [state]
-     (listen-all)
+     (listen-all!)
      state)})
 
 (defn refresh-internal!
@@ -182,27 +243,29 @@
   (when-not (:ui/shortcut-handler-refreshing? @state/state)
     (state/set-state! :ui/shortcut-handler-refreshing? true)
 
-    (doseq [id (keys @*installed)]
-      (uninstall-shortcut-handler! id))
-    (install-shortcuts!)
+    (let [ids (keys @*installed-handlers)
+          _handler-ids (set (map :group (vals @*installed-handlers)))]
+      (doseq [id ids] (uninstall-shortcut-handler! id))
+      ;; TODO: should re-install existed handlers
+      (install-shortcuts! nil))
     (state/pub-event! [:shortcut-handler-refreshed])
     (state/set-state! :ui/shortcut-handler-refreshing? false)))
 
 (def refresh! (debounce refresh-internal! 1000))
 
 (defn- name-with-meta [e]
-  (let [ctrl    (.-ctrlKey e)
-        alt     (.-altKey e)
-        meta    (.-metaKey e)
-        shift   (.-shiftKey e)
+  (let [ctrl (.-ctrlKey e)
+        alt (.-altKey e)
+        meta (.-metaKey e)
+        shift (.-shiftKey e)
         keyname (get key-names (str (.-keyCode e)))]
     (cond->> keyname
-      ctrl  (str "ctrl+")
-      alt   (str "alt+")
-      meta  (str "meta+")
-      shift (str "shift+"))))
+             ctrl (str "ctrl+")
+             alt (str "alt+")
+             meta (str "meta+")
+             shift (str "shift+"))))
 
-(defn- keyname [e]
+(defn keyname [e]
   (let [name (get key-names (str (.-keyCode e)))]
     (case name
       nil nil
@@ -215,7 +278,7 @@
      (let [handler (KeyHandler. js/document)
            keystroke (:rum/local state)]
 
-       (doseq [id (keys @*installed)]
+       (doseq [id (keys @*installed-handlers)]
          (uninstall-shortcut-handler! id))
 
        (events/listen handler "key"
@@ -240,6 +303,27 @@
      (when-let [^js handler (::key-record-handler state)]
        (.dispose handler))
 
+     ;; force re-install shortcut handlers
      (js/setTimeout #(refresh!) 500)
 
      (dissoc state ::key-record-handler))})
+
+(defn persist-user-shortcut!
+  [id binding]
+  (let [graph-shortcuts (or (:shortcuts (state/get-graph-config)) {})
+        global-shortcuts (or (:shortcuts (state/get-global-config)) {})
+        global? true]
+    (letfn [(into-shortcuts [shortcuts]
+              (cond-> shortcuts
+                      (nil? binding)
+                      (dissoc id)
+
+                      (and global?
+                           (or (string? binding)
+                               (vector? binding)
+                               (boolean? binding)))
+                      (assoc id binding)))]
+      ;; TODO: exclude current graph config shortcuts
+      (when (nil? binding)
+        (config-handler/set-config! :shortcuts (into-shortcuts graph-shortcuts)))
+      (global-config-handler/set-global-config-kv! :shortcuts (into-shortcuts global-shortcuts)))))

+ 173 - 73
src/main/frontend/modules/shortcut/data_helper.cljs

@@ -1,11 +1,14 @@
 (ns frontend.modules.shortcut.data-helper
   (:require [borkdude.rewrite-edn :as rewrite]
+            [clojure.set :refer [rename-keys] :as set]
             [clojure.string :as str]
-            [clojure.set :refer [rename-keys]]
+            [cljs-bean.core :as bean]
+            [frontend.context.i18n :refer [t]]
             [frontend.config :as config]
             [frontend.db :as db]
             [frontend.handler.file :as file]
             [frontend.modules.shortcut.config :as shortcut-config]
+            [frontend.modules.shortcut.utils :as shortcut-utils]
             [frontend.state :as state]
             [frontend.util :as util]
             [lambdaisland.glogi :as log]
@@ -13,29 +16,74 @@
             [frontend.handler.config :as config-handler])
   (:import [goog.ui KeyboardShortcutHandler]))
 
+(declare get-group)
+
 ;; function vals->bindings is too time-consuming. Here we cache the results.
-(defn- flatten-key-bindings
-  [config]
-  (->> config
-       (into {})
-       (map (fn [[k {:keys [binding]}]]
-              {k binding}))
+(defn- flatten-bindings-by-id
+  [config user-shortcuts binding-only?]
+  (->> (vals config)
+       (apply merge)
+       (map (fn [[id {:keys [binding] :as opts}]]
+              {id (if binding-only?
+                    (get user-shortcuts id binding)
+                    (assoc opts :user-binding (get user-shortcuts id)
+                                :handler-id (get-group id)
+                                :id id))}))
        (into {})))
 
-(def m-flatten-key-bindings (util/memoize-last flatten-key-bindings))
+(defn- flatten-bindings-by-key
+  [config user-shortcuts]
+  (reduce-kv
+    (fn [r handler-id vs]
+      (reduce-kv
+        (fn [r id {:keys [binding]}]
+          (if-let [ks (get user-shortcuts id binding)]
+            (let [ks (if (sequential? ks) ks [ks])]
+              (reduce (fn [a k]
+                        (let [k (shortcut-utils/undecorate-binding k)
+                              k' (shortcut-utils/safe-parse-string-binding k)
+                              k' (bean/->clj k')]
+                          (-> a
+                              (assoc-in [k' :key] k)
+                              (assoc-in [k' :refs id] handler-id)))) r ks))
+            r)) r vs))
+    {} config))
+
+(def m-flatten-bindings-by-id
+  (util/memoize-last flatten-bindings-by-id))
+
+(def m-flatten-bindings-by-key
+  (util/memoize-last flatten-bindings-by-key))
 
 (defn get-bindings
   []
-  (m-flatten-key-bindings (vals @shortcut-config/config)))
+  (m-flatten-bindings-by-id @shortcut-config/*config (state/shortcuts) true))
 
-(defn- mod-key [shortcut]
-  (str/replace shortcut #"(?i)mod"
-               (if util/mac? "meta" "ctrl")))
+(defn get-bindings-keys-map
+  []
+  (m-flatten-bindings-by-key @shortcut-config/*config (state/shortcuts)))
+
+(defn get-bindings-ids-map
+  []
+  (m-flatten-bindings-by-id @shortcut-config/*config (state/shortcuts) false))
+
+(defn get-shortcut-desc
+  [binding-map]
+  (let [{:keys [id desc cmd]} binding-map
+        desc (or desc (:desc cmd) (some-> id (shortcut-utils/decorate-namespace) (t)))]
+    (if (or (nil? desc)
+            (and (string? desc) (str/starts-with? desc "{Missing")))
+      (str id) desc)))
+
+(defn mod-key [shortcut]
+  (when (string? shortcut)
+    (str/replace shortcut #"(?i)mod"
+                 (if util/mac? "meta" "ctrl"))))
 
 (defn shortcut-binding
+  "override by user custom binding"
   [id]
-  (let [shortcut (get (state/shortcuts) id
-                      (get (get-bindings) id))]
+  (let [shortcut (get (get-bindings) id)]
     (cond
       (nil? shortcut)
       (log/warn :shortcut/binding-not-found {:id id})
@@ -47,62 +95,48 @@
 
       :else
       (->>
-       (if (string? shortcut)
-         [shortcut]
-         shortcut)
-       (mapv mod-key)))))
+        (if (string? shortcut)
+          [shortcut]
+          shortcut)
+        (mapv mod-key)))))
 
 (defn shortcut-cmd
   [id]
   (get @shortcut-config/*shortcut-cmds id))
 
+(defn shortcut-item
+  [id]
+  (get (get-bindings-ids-map) id))
+
 ;; returns a vector to preserve order
 (defn binding-by-category [name]
-  (let [dict    (->> (vals @shortcut-config/config)
-                     (apply merge)
-                     (map (fn [[k _]]
-                            {k {:binding (shortcut-binding k)}}))
-                     (into {}))
+  (let [dict (get-bindings-ids-map)
         plugin? (= name :shortcut.category/plugins)]
     (->> (if plugin?
            (->> (keys dict) (filter #(str/starts-with? (str %) ":plugin.")))
-           (shortcut-config/category name))
-         (mapv (fn [k] [k (k dict)])))))
+           (shortcut-config/get-category-shortcuts name))
+         (mapv (fn [k] [k (assoc (get dict k) :category name)])))))
 
 (defn shortcut-map
   ([handler-id]
    (shortcut-map handler-id nil))
   ([handler-id state]
-   (let [raw       (get @shortcut-config/config handler-id)
+   (let [raw (get @shortcut-config/*config handler-id)
          handler-m (->> raw
                         (map (fn [[k {:keys [fn]}]]
                                {k fn}))
                         (into {}))
-         before    (-> raw meta :before)]
+         before (-> raw meta :before)]
      (cond->> handler-m
-       state  (reduce-kv (fn [r k handle-fn]
-                           (assoc r k (partial handle-fn state)))
-                         {})
-       before (reduce-kv (fn [r k v]
-                           (assoc r k (before v)))
-                         {})))))
-
-(defn decorate-namespace [k]
-  (let [n (name k)
-        ns (namespace k)]
-    (keyword (str "command." ns) n)))
-
-(defn decorate-binding [binding]
-  (-> (if (string? binding) binding (str/join "+"  binding))
-      (str/replace "mod" (if util/mac? "⌘" "ctrl"))
-      (str/replace "alt" (if util/mac? "opt" "alt"))
-      (str/replace "shift+/" "?")
-      (str/replace "left" "←")
-      (str/replace "right" "→")
-      (str/replace "shift" "⇧")
-      (str/replace "open-square-bracket" "[")
-      (str/replace "close-square-bracket" "]")
-      (str/lower-case)))
+              state (reduce-kv (fn [r k handle-fn]
+                                 (let [handle-fn' (if (volatile? state)
+                                                    (fn [*state & args] (apply handle-fn (cons @*state args)))
+                                                    handle-fn)]
+                                   (assoc r k (partial handle-fn' state))))
+                               {})
+              before (reduce-kv (fn [r k v]
+                                  (assoc r k (before v)))
+                                {})))))
 
 ;; if multiple bindings, gen seq for first binding only for now
 (defn gen-shortcut-seq [id]
@@ -111,24 +145,24 @@
       []
       (-> bindings
           first
-          (str/split  #" |\+")))))
+          (str/split #" |\+")))))
 
 (defn binding-for-display [k binding]
   (let [tmp (cond
               (false? binding)
               (cond
-                (and util/mac? (= k :editor/kill-line-after))    "system default: ctrl+k"
+                (and util/mac? (= k :editor/kill-line-after)) "system default: ctrl+k"
                 (and util/mac? (= k :editor/beginning-of-block)) "system default: ctrl+a"
-                (and util/mac? (= k :editor/end-of-block))       "system default: ctrl+e"
+                (and util/mac? (= k :editor/end-of-block)) "system default: ctrl+e"
                 (and util/mac? (= k :editor/backward-kill-word)) "system default: opt+delete"
-                :else "disabled")
+                :else (t :keymap/disabled))
 
               (string? binding)
-              (decorate-binding binding)
+              (shortcut-utils/decorate-binding binding)
 
               :else
               (->> binding
-                   (map decorate-binding)
+                   (map shortcut-utils/decorate-binding)
                    (str/join " | ")))]
 
     ;; Display "cmd" rather than "meta" to the user to describe the Mac
@@ -157,26 +191,92 @@
   "Given shortcut key, return handler group
   eg: :editor/new-line -> :shortcut.handler/block-editing-only"
   [k]
-  (->> @shortcut-config/config
+  (->> @shortcut-config/*config
        (filter (fn [[_ v]] (contains? v k)))
        (map key)
        (first)))
 
-(defn potential-conflict? [k]
-  (if-not (shortcut-binding k)
+(defn should-be-included-to-global-handler
+  [from-handler-id]
+  (if (contains? #{:shortcut.handler/pdf} from-handler-id)
+    #{from-handler-id :shortcut.handler/global-prevent-default}
+    #{from-handler-id}))
+
+(defn get-conflicts-by-keys
+  ([ks] (get-conflicts-by-keys ks :shortcut.handler/global-prevent-default {:group-global? true}))
+  ([ks handler-id] (get-conflicts-by-keys ks handler-id {:group-global? true}))
+  ([ks handler-id {:keys [exclude-ids group-global?]}]
+   (let [global-handlers #{:shortcut.handler/editor-global
+                           :shortcut.handler/global-non-editing-only
+                           :shortcut.handler/global-prevent-default
+                           :shortcut.handler/misc}
+         ks-bindings (get-bindings-keys-map)
+         handler-ids (should-be-included-to-global-handler handler-id)
+         global? (when group-global? (seq (set/intersection global-handlers handler-ids)))]
+     (->> (if (string? ks) [ks] ks)
+          (map (fn [k]
+                 (when-let [k' (shortcut-utils/undecorate-binding k)]
+                   (let [k (shortcut-utils/safe-parse-string-binding k')
+                         k (bean/->clj k)
+
+                         same-leading-key?
+                         (fn [[k' _]]
+                           (when (sequential? k)
+                             (or (= k k')
+                                 (and (> (count k') (count k))
+                                      (= (first k) (first k'))))))
+
+                         into-conflict-refs
+                         (fn [[k o]]
+                           (when-let [{:keys [key refs]} o]
+                             [k [key (reduce-kv (fn [r id handler-id']
+                                                  (if (and
+                                                        (not (contains? exclude-ids id))
+                                                        (or (= handler-ids #{handler-id'})
+                                                            (and (set? handler-ids) (contains? handler-ids handler-id'))
+                                                            (and global? (contains? global-handlers handler-id'))))
+                                                    (assoc r id handler-id')
+                                                    r)
+                                                  ) {} refs)]]))]
+
+                     [k' (->> ks-bindings
+                              (filterv same-leading-key?)
+                              (mapv into-conflict-refs)
+                              (remove #(empty? (second (second %1))))
+                              (into {}))]
+                     ))))
+          (remove #(empty? (vals (second %1))))
+          (into {})))))
+
+(defn parse-conflicts-from-binding
+  [from-binding target]
+  (when-let [from-binding (and (string? target)
+                               (sequential? from-binding)
+                               (seq from-binding))]
+    (when-let [target (some-> target (mod-key) (shortcut-utils/safe-parse-string-binding) (bean/->clj))]
+      (->> from-binding
+           (filterv
+             #(when-let [from (some-> % (mod-key) (shortcut-utils/safe-parse-string-binding) (bean/->clj))]
+                (or (= from target)
+                    (and (or (= (count from) 1)
+                             (= (count target) 1))
+                         (= (first target) (first from))))))))))
+
+(defn potential-conflict? [shortcut-id]
+  (if-not (shortcut-binding shortcut-id)
     false
-    (let [handler-id    (get-group k)
-          shortcut-m    (shortcut-map handler-id)
+    (let [handler-id (get-group shortcut-id)
+          shortcut-m (shortcut-map handler-id)
           parse-shortcut #(try
-                           (KeyboardShortcutHandler/parseStringShortcut %)
-                           (catch :default e
-                             (js/console.error "[shortcut/parse-error]" (str % " - " (.-message e)))))
-          bindings      (->> (shortcut-binding k)
-                             (map mod-key)
-                             (map parse-shortcut)
-                             (map js->clj))
+                            (KeyboardShortcutHandler/parseStringShortcut %)
+                            (catch :default e
+                              (js/console.error "[shortcut/parse-error]" (str % " - " (.-message e)))))
+          bindings (->> (shortcut-binding shortcut-id)
+                        (map mod-key)
+                        (map parse-shortcut)
+                        (map js->clj))
           rest-bindings (->> (map key shortcut-m)
-                             (remove #{k})
+                             (remove #{shortcut-id})
                              (map shortcut-binding)
                              (filter vector?)
                              (mapcat identity)
@@ -188,16 +288,16 @@
 
 (defn shortcut-data-by-id [id]
   (let [binding (shortcut-binding id)
-        data    (->> (vals @shortcut-config/config)
-                     (into  {})
-                     id)]
+        data (->> (vals @shortcut-config/*config)
+                  (into {})
+                  id)]
     (assoc
       data
       :binding
       (binding-for-display id binding))))
 
 (defn shortcuts->commands [handler-id]
-  (let [m (get @shortcut-config/config handler-id)]
+  (let [m (get @shortcut-config/*config handler-id)]
     (->> m
          (map (fn [[id _]] (-> (shortcut-data-by-id id)
                                (assoc :id id :handler-id handler-id)

+ 58 - 0
src/main/frontend/modules/shortcut/utils.cljs

@@ -0,0 +1,58 @@
+(ns frontend.modules.shortcut.utils
+  (:require [clojure.string :as str]
+            [frontend.util :as util])
+  (:import [goog.ui KeyboardShortcutHandler]))
+
+(defn safe-parse-string-binding
+  [binding]
+  (try
+    (KeyboardShortcutHandler/parseStringShortcut binding)
+    (catch js/Error e
+      (js/console.warn "[shortcuts] parse key error: " e) binding)))
+
+(defn mod-key [binding]
+  (str/replace binding #"(?i)mod"
+               (if util/mac? "meta" "ctrl")))
+
+(defn undecorate-binding
+  [binding]
+  (when (string? binding)
+    (let [keynames {";" "semicolon"
+                    "=" "equals"
+                    "-" "dash"
+                    "[" "open-square-bracket"
+                    "]" "close-square-bracket"
+                    "'" "single-quote"
+                    "(" "shift+9"
+                    ")" "shift+0"
+                    "~" "shift+`"
+                    "⇧" "shift"
+                    "←" "left"
+                    "→" "right"}]
+      (-> binding
+          (str/replace #"[;=-\[\]'\(\)\~\→\←\⇧]" #(get keynames %))
+          (str/replace #"\s+" " ")
+          (mod-key)
+          (str/lower-case)))))
+
+(defn decorate-namespace [k]
+  (let [n (name k)
+        ns (namespace k)]
+    (keyword (str "command." ns) n)))
+
+(defn decorate-binding [binding]
+  (when (or (string? binding)
+            (sequential? binding))
+    (-> (if (string? binding) binding (str/join "+" binding))
+        (str/replace "mod" (if util/mac? "⌘" "ctrl"))
+        (str/replace "meta" (if util/mac? "⌘" "⊞ win"))
+        (str/replace "alt" (if util/mac? "opt" "alt"))
+        (str/replace "shift+/" "?")
+        (str/replace "left" "←")
+        (str/replace "right" "→")
+        (str/replace "shift" "⇧")
+        (str/replace "open-square-bracket" "[")
+        (str/replace "close-square-bracket" "]")
+        (str/replace "equals" "=")
+        (str/replace "semicolon" ";")
+        (str/lower-case))))

+ 0 - 5
src/main/frontend/routes.cljs

@@ -9,7 +9,6 @@
             [frontend.components.repo :as repo]
             [frontend.components.search :as search]
             [frontend.components.settings :as settings]
-            [frontend.components.shortcut :as shortcut]
             [frontend.components.whiteboard :as whiteboard] 
             [frontend.extensions.zotero :as zotero]
             [frontend.components.bug-report :as bug-report]
@@ -69,10 +68,6 @@
     {:name :settings
      :view settings/settings}]
 
-   ["/settings/shortcut"
-    {:name :shortcut-setting
-     :view shortcut/shortcut-page}]
-
    ["/settings/zotero"
     {:name :zotero-setting
      :view zotero/settings}]

+ 30 - 12
src/main/frontend/state.cljs

@@ -61,6 +61,7 @@
      :modal/label                           ""
      :modal/show?                           false
      :modal/panel-content                   nil
+     :modal/payload                         nil
      :modal/fullscreen?                     false
      :modal/close-btn?                      nil
      :modal/close-backdrop?                 true
@@ -344,6 +345,18 @@
              (merge current new)
              new)))))
 
+(defn get-global-config
+  []
+  (get-in @state [:config ::global-config]))
+
+(defn get-global-config-str-content
+  []
+  (get-in @state [:config ::global-config-str-content]))
+
+(defn get-graph-config
+  ([] (get-graph-config (get-current-repo)))
+  ([repo-url] (get-in @state [:config repo-url])))
+
 (defn get-config
   "User config for the given repo or current repo if none given. All config fetching
 should be done through this fn in order to get global config and config defaults"
@@ -352,8 +365,8 @@ should be done through this fn in order to get global config and config defaults
   ([repo-url]
    (merge-configs
     default-config
-    (get-in @state [:config ::global-config])
-    (get-in @state [:config repo-url]))))
+    (get-global-config)
+    (get-graph-config repo-url))))
 
 (defonce publishing? (atom nil))
 
@@ -1341,7 +1354,7 @@ Similar to re-frame subscriptions"
   ([panel-content]
    (set-sub-modal! panel-content
                    {:close-btn? true}))
-  ([panel-content {:keys [id label close-btn? close-backdrop? show? center?] :as opts}]
+  ([panel-content {:keys [id label payload close-btn? close-backdrop? show? center?] :as opts}]
    (if (not (modal-opened?))
      (set-modal! panel-content opts)
      (let [modals (:modal/subsets @state)
@@ -1351,6 +1364,7 @@ Similar to re-frame subscriptions"
                    #(not (nil? %1))
                    {:modal/id            id
                     :modal/label         (or label (if center? "ls-modal-align-center" ""))
+                    :modal/payload       payload
                     :modal/show?         (if (boolean? show?) show? true)
                     :modal/panel-content panel-content
                     :modal/close-btn?    close-btn?
@@ -1380,7 +1394,7 @@ Similar to re-frame subscriptions"
    (set-modal! modal-panel-content
                {:fullscreen? false
                 :close-btn?  true}))
-  ([modal-panel-content {:keys [id label fullscreen? close-btn? close-backdrop? center?]}]
+  ([modal-panel-content {:keys [id label payload fullscreen? close-btn? close-backdrop? center?]}]
    (let [opened? (modal-opened?)]
      (when opened?
        (close-modal!))
@@ -1395,6 +1409,7 @@ Similar to re-frame subscriptions"
               :modal/label (or label (if center? "ls-modal-align-center" ""))
               :modal/show? (boolean modal-panel-content)
               :modal/panel-content modal-panel-content
+              :modal/payload payload
               :modal/fullscreen? fullscreen?
               :modal/close-btn? close-btn?
               :modal/close-backdrop? (if (boolean? close-backdrop?) close-backdrop? true))))
@@ -1408,6 +1423,7 @@ Similar to re-frame subscriptions"
       (swap! state assoc
              :modal/id nil
              :modal/label ""
+             :modal/payload nil
              :modal/show? false
              :modal/fullscreen? false
              :modal/panel-content nil
@@ -1477,9 +1493,11 @@ Similar to re-frame subscriptions"
   (when value (set-state! [:config repo-url] value)))
 
 (defn set-global-config!
-  [value]
+  [value str-content]
   ;; Placed under :config so cursors can work seamlessly
-  (when value (set-config! ::global-config value)))
+  (when value
+    (set-config! ::global-config value)
+    (set-config! ::global-config-str-content str-content)))
 
 (defn get-wide-mode?
   []
@@ -1499,13 +1517,13 @@ Similar to re-frame subscriptions"
 
 (defn get-plugins-commands-with-type
   [type]
-  (filterv #(= (keyword (first %)) (keyword type))
-           (apply concat (vals (:plugin/simple-commands @state)))))
+  (->> (apply concat (vals (:plugin/simple-commands @state)))
+       (filterv #(= (keyword (first %)) (keyword type)))))
 
 (defn get-plugins-ui-items-with-type
   [type]
-  (filterv #(= (keyword (first %)) (keyword type))
-           (apply concat (vals (:plugin/installed-ui-items @state)))))
+  (->> (apply concat (vals (:plugin/installed-ui-items @state)))
+       (filterv #(= (keyword (first %)) (keyword type)))))
 
 (defn get-plugin-resources-with-type
   [pid type]
@@ -1734,8 +1752,8 @@ Similar to re-frame subscriptions"
   (set-state! :ui/settings-open? false))
 
 (defn open-settings!
-  []
-  (set-state! :ui/settings-open? true))
+  ([] (open-settings! true))
+  ([active-tab] (set-state! :ui/settings-open? active-tab)))
 
 ;; TODO: Move those to the uni `state`
 

+ 7 - 6
src/main/frontend/ui.cljs

@@ -21,7 +21,7 @@
             [frontend.mobile.util :as mobile-util]
             [frontend.modules.shortcut.config :as shortcut-config]
             [frontend.modules.shortcut.core :as shortcut]
-            [frontend.modules.shortcut.data-helper :as shortcut-helper]
+            [frontend.modules.shortcut.utils :as shortcut-utils]
             [frontend.rum :as r]
             [frontend.state :as state]
             [frontend.storage :as storage]
@@ -167,7 +167,7 @@
                    sequence)]
     [:span.keyboard-shortcut
      (map-indexed (fn [i key]
-                    (let [key' (shortcut-helper/decorate-binding (str key))]
+                    (let [key' (shortcut-utils/decorate-binding (str key))]
                       [:code {:key i}
                       ;; Display "cmd" rather than "meta" to the user to describe the Mac
                       ;; mod key, because that's what the Mac keyboards actually say.
@@ -507,7 +507,7 @@
 
 (rum/defcs auto-complete <
   (rum/local 0 ::current-idx)
-  (shortcut/mixin :shortcut.handler/auto-complete)
+  (shortcut/mixin* :shortcut.handler/auto-complete)
   [state
    matched
    {:keys [on-chosen
@@ -567,9 +567,10 @@
        :aria-hidden "true"}]]]))
 
 (defn keyboard-shortcut-from-config [shortcut-name]
-  (let [default-binding (:binding (get shortcut-config/all-default-keyboard-shortcuts shortcut-name))
-        custom-binding  (when (state/shortcuts) (get (state/shortcuts) shortcut-name))]
-    (or custom-binding default-binding)))
+  (let [built-in-binding (:binding (get shortcut-config/all-built-in-keyboard-shortcuts shortcut-name))
+        custom-binding  (when (state/shortcuts) (get (state/shortcuts) shortcut-name))
+        binding         (or custom-binding built-in-binding)]
+    (shortcut-utils/decorate-binding binding)))
 
 (rum/defc modal-overlay
   [state close-fn close-backdrop?]

+ 1 - 1
src/main/frontend/ui/date_picker.cljs

@@ -172,7 +172,7 @@
   {:init (fn [state]
            (reset! *internal-model (first (:rum/args state)))
            state)}
-  (shortcut/mixin :shortcut.handler/date-picker)
+  (shortcut/mixin :shortcut.handler/date-picker false)
   [_model {:keys [on-change disabled? start-of-week class style attr]
            :or   {start-of-week (state/get-start-of-week)} ;; Default to Sunday
            :as   args}]

+ 0 - 13
src/main/frontend/util.cljc

@@ -67,19 +67,6 @@
   [parts]
   (string/join "/" parts))
 
-(defn normalize-user-keyname
-  [k]
-  (let [keynames {";" "semicolon"
-                  "=" "equals"
-                  "-" "dash"
-                  "[" "open-square-bracket"
-                  "]" "close-square-bracket"
-                  "'" "single-quote"}]
-    (some-> (str k)
-            (string/lower-case)
-            (string/replace #"[;=-\[\]']" (fn [s]
-                                            (get keynames s))))))
-
 #?(:cljs
    (defn safe-re-find
      {:malli/schema [:=> [:cat :any :string] [:or :nil :string [:vector [:maybe :string]]]]}

+ 4 - 4
src/main/logseq/api.cljs

@@ -367,9 +367,9 @@
                                (if palette?
                                  (palette-handler/invoke-command palette-cmd)
                                  (action')))
-                [handler-id id shortcut-map] (update shortcut-args 2 assoc :fn dispatch-cmd :cmd palette-cmd)]
-            (println :shortcut/register-shortcut [handler-id id shortcut-map])
-            (st/register-shortcut! handler-id id shortcut-map)))))))
+                [mode-id id shortcut-map] (update shortcut-args 2 merge cmd {:fn dispatch-cmd :cmd palette-cmd})]
+            (println :shortcut/register-shortcut [mode-id id shortcut-map])
+            (st/register-shortcut! mode-id id shortcut-map)))))))
 
 (defn ^:export unregister_plugin_simple_command
   [pid]
@@ -422,7 +422,7 @@
                            (util/safe-lower-case)
                            (keyword)))]
       (when-let [action (get-in (palette-handler/get-commands-unique) [id :action])]
-        (apply action args)))))
+        (apply plugin-handler/hook-lifecycle-fn! id action args)))))
 
 ;; flag - boolean | 'toggle'
 (def ^:export set_left_sidebar_visible

+ 17 - 1
src/resources/dicts/en.edn

@@ -320,6 +320,7 @@
  :settings-page/current-version "Current version"
  :settings-page/tab-general "General"
  :settings-page/tab-editor "Editor"
+ :settings-page/tab-keymap "Keymap"
  :settings-page/tab-version-control "Version control"
  :settings-page/tab-account "Account"
  :settings-page/tab-advanced "Advanced"
@@ -359,6 +360,7 @@
  :close "Close"
  :delete "Delete"
  :save "Save"
+ :reset "Reset"
  :type "Type"
  :host "Host"
  :port "Port"
@@ -636,9 +638,23 @@
  :shortcut.category/block-command-editing "Block command editing"
  :shortcut.category/block-selection "Block selection (press Esc to quit selection)"
  :shortcut.category/toggle "Toggle"
- :shortcut.category/whiteboard "Whiteboard"
  :shortcut.category/others "Others"
  :shortcut.category/plugins "Plugins"
+ :shortcut.category/whiteboard "Whiteboard"
+
+ :keymap/all "All"
+ :keymap/disabled "Disabled"
+ :keymap/unset "Unset"
+ :keymap/custom "Custom"
+ :keymap/search "Search"
+ :keymap/total "Total shortcuts"
+ :keymap/keystroke-filter "Keystroke filter"
+ :keymap/keystroke-record-desc "Press any sequence of keys to filter shortcuts"
+ :keymap/keystroke-record-setup-label "Press any sequence of keys to set a shortcut"
+ :keymap/restore-to-default "Restore to system default"
+ :keymap/customize-for-label "Customize shortcuts"
+ :keymap/conflicts-for-label "Keymap conflicts for"
+
  :window/minimize "Minimize"
  :window/maximize "Maximize"
  :window/restore "Restore"

+ 18 - 0
src/resources/dicts/zh-cn.edn

@@ -233,6 +233,7 @@
  :settings-page/show-full-blocks "显示块引用的所有行"
  :settings-page/tab-general "常规"
  :settings-page/tab-editor "编辑器"
+ :settings-page/tab-keymap "快捷键"
  :settings-page/tab-assets "附件设置"
  :settings-page/tab-advanced "高级设置"
  :settings-page/tab-features "更多功能"
@@ -293,6 +294,7 @@
  :close "关闭"
  :delete "删除"
  :save "保存"
+ :reset "重设"
  :type "类型"
  :host "主机"
  :port "端口"
@@ -486,6 +488,22 @@
  :shortcut.category/block-selection       "块选择操作"
  :shortcut.category/toggle                "切换"
  :shortcut.category/others                "其他"
+ :shortcut.category/plugins               "插件"
+ :shortcut.category/whiteboard            "白板"
+
+ :keymap/all "全部"
+ :keymap/disabled "已禁用"
+ :keymap/unset "未设置"
+ :keymap/custom "自定义"
+ :keymap/search "搜索"
+ :keymap/total "共计条目"
+ :keymap/keystroke-filter "按键过滤"
+ :keymap/keystroke-record-desc "请敲击键盘提供按键组合, 以过滤快捷键。"
+ :keymap/keystroke-record-setup-label "请敲击键盘提供按键组合"
+ :keymap/restore-to-default "恢复到默认映射"
+ :keymap/customize-for-label "自定义快捷键"
+ :keymap/conflicts-for-label "发现映射冲突 "
+
  :command.auto-complete/complete          "自动完成:选择当前项"
  :command.auto-complete/next              "自动完成:选择下一项"
  :command.auto-complete/open-link         "自动完成:在浏览器中打开当前项"

+ 49 - 0
src/test/frontend/modules/shortcut/core_test.cljs

@@ -0,0 +1,49 @@
+(ns frontend.modules.shortcut.core-test
+  (:require [cljs.test :refer [deftest is testing]]
+            [clojure.string :as string]
+            [frontend.modules.shortcut.data-helper :as dh]
+            [frontend.util :as util]))
+
+(deftest test-core-basic
+  (testing "get handler id"
+    (is (= (dh/get-group :editor/copy) :shortcut.handler/editor-global))))
+
+(deftest test-shortcut-conflicts-detection
+  (testing "get conflicts with shortcut id")
+
+  (testing "get conflicts with binding keys"
+    (is (= (count (dh/get-conflicts-by-keys "mod+c")) 1))
+
+    (is (contains?
+          (->> (dh/get-conflicts-by-keys
+                 "mod+c" :shortcut.handler/editor-global
+                 {:exclude-ids #{:editor/copy} :group-global? true})
+               (vals) (mapcat #(vals %)) (some #(when (= (first %) (if util/mac? "meta+c" "ctrl+c")) (second %))))
+          :misc/copy))
+
+    (is (->> (dh/get-conflicts-by-keys ["t"])
+             (vals)
+             (first)
+             (vals)
+             (map first)
+             (every? #(string/starts-with? % "t")))
+        "get the conflicts from the only leader key")
+
+    (is (nil? (seq (dh/get-conflicts-by-keys ["g"] :shortcut.handler/cards)))
+        "specific handler with the global conflicting key"))
+
+  (testing "parse conflicts from the string binding list"
+    (is (= (dh/parse-conflicts-from-binding ["g" "g t"] "g")
+           ["g" "g t"]))
+
+    (is (= (dh/parse-conflicts-from-binding ["g" "g t" "t r"] "g t")
+           ["g" "g t"]))
+
+    (is (= (dh/parse-conflicts-from-binding ["g" "g t" "t r"] "g x")
+           ["g"]))
+
+    (is (= (dh/parse-conflicts-from-binding ["meta+x" "meta+x t" "t r"] "meta+x x")
+           ["meta+x"]))))
+
+(comment
+  (cljs.test/run-tests))

+ 44 - 44
src/test/frontend/util_test.cljs

@@ -25,9 +25,9 @@
 (deftest test-memoize-last
   (testing "memoize-last add test"
     (let [actual-ops (atom 0)
-          m+         (util/memoize-last (fn [x1 x2]
-                                           (swap! actual-ops inc) ;; side effect for counting
-                                           (+ x1 x2)))]
+          m+ (util/memoize-last (fn [x1 x2]
+                                  (swap! actual-ops inc)    ;; side effect for counting
+                                  (+ x1 x2)))]
       (is (= (m+ 1 1) 2))
       (is (= @actual-ops 1))
       (is (= (m+ 1 1) 2))
@@ -44,58 +44,58 @@
 
   (testing "memoize-last nested mapping test"
     (let [actual-ops (atom 0)
-          flatten-f  (util/memoize-last (fn [& args]
-                                           (swap! actual-ops inc) ;; side effect for counting
-                                           (apply #'shortcut-data-helper/flatten-key-bindings args)))
-          target     (atom {:part1 {:date-picker/complete         {:binding "enter"
-                                                                   :fn      "ui-handler/shortcut-complete"}
-                                    :date-picker/prev-day         {:binding "left"
-                                                                   :fn      "ui-handler/shortcut-prev-day"}}
-                            :part2 {:date-picker/next-day         {:binding "right"
-                                                                   :fn      "ui-handler/shortcut-next-day"}
-                                    :date-picker/prev-week        {:binding ["up" "ctrl+p"]
-                                                                   :fn      "ui-handler/shortcut-prev-week"}}})]
-      (is (= (flatten-f (vals @target)) {:date-picker/complete         "enter"
-                                         :date-picker/prev-day         "left"
-                                         :date-picker/next-day         "right"
-                                         :date-picker/prev-week        ["up" "ctrl+p"]}))
+          flatten-f (util/memoize-last (fn [& args]
+                                         (swap! actual-ops inc) ;; side effect for counting
+                                         (apply #'shortcut-data-helper/flatten-bindings-by-id (conj (vec args) nil true))))
+          target (atom {:part1 {:date-picker/complete {:binding "enter"
+                                                       :fn      "ui-handler/shortcut-complete"}
+                                :date-picker/prev-day {:binding "left"
+                                                       :fn      "ui-handler/shortcut-prev-day"}}
+                        :part2 {:date-picker/next-day  {:binding "right"
+                                                        :fn      "ui-handler/shortcut-next-day"}
+                                :date-picker/prev-week {:binding ["up" "ctrl+p"]
+                                                        :fn      "ui-handler/shortcut-prev-week"}}})]
+      (is (= (flatten-f @target) {:date-picker/complete  "enter"
+                                  :date-picker/prev-day  "left"
+                                  :date-picker/next-day  "right"
+                                  :date-picker/prev-week ["up" "ctrl+p"]}))
       (is (= @actual-ops 1))
-      (is (= (flatten-f (vals @target)) {:date-picker/complete         "enter"
-                                         :date-picker/prev-day         "left"
-                                         :date-picker/next-day         "right"
-                                         :date-picker/prev-week        ["up" "ctrl+p"]}))
+      (is (= (flatten-f @target) {:date-picker/complete  "enter"
+                                  :date-picker/prev-day  "left"
+                                  :date-picker/next-day  "right"
+                                  :date-picker/prev-week ["up" "ctrl+p"]}))
       (is (= @actual-ops 1))
       ;; edit value
       (swap! target assoc-in [:part1 :date-picker/complete :binding] "tab")
-      (is (= (flatten-f (vals @target)) {:date-picker/complete         "tab"
-                                         :date-picker/prev-day         "left"
-                                         :date-picker/next-day         "right"
-                                         :date-picker/prev-week        ["up" "ctrl+p"]}))
+      (is (= (flatten-f @target) {:date-picker/complete  "tab"
+                                  :date-picker/prev-day  "left"
+                                  :date-picker/next-day  "right"
+                                  :date-picker/prev-week ["up" "ctrl+p"]}))
       (is (= @actual-ops 2))
-      (is (= (flatten-f (vals @target)) {:date-picker/complete         "tab"
-                                         :date-picker/prev-day         "left"
-                                         :date-picker/next-day         "right"
-                                         :date-picker/prev-week        ["up" "ctrl+p"]}))
+      (is (= (flatten-f @target) {:date-picker/complete  "tab"
+                                  :date-picker/prev-day  "left"
+                                  :date-picker/next-day  "right"
+                                  :date-picker/prev-week ["up" "ctrl+p"]}))
       (is (= @actual-ops 2))
-      (is (= (flatten-f (vals @target)) {:date-picker/complete         "tab"
-                                         :date-picker/prev-day         "left"
-                                         :date-picker/next-day         "right"
-                                         :date-picker/prev-week        ["up" "ctrl+p"]}))
+      (is (= (flatten-f @target) {:date-picker/complete  "tab"
+                                  :date-picker/prev-day  "left"
+                                  :date-picker/next-day  "right"
+                                  :date-picker/prev-week ["up" "ctrl+p"]}))
       (is (= @actual-ops 2))
       ;; edit key
       (swap! target assoc :part3 {:date-picker/next-week {:binding "down"
                                                           :fn      "ui-handler/shortcut-next-week"}})
-      (is (= (flatten-f (vals @target)) {:date-picker/complete         "tab"
-                                         :date-picker/prev-day         "left"
-                                         :date-picker/next-day         "right"
-                                         :date-picker/prev-week        ["up" "ctrl+p"]
-                                         :date-picker/next-week        "down"}))
+      (is (= (flatten-f @target) {:date-picker/complete  "tab"
+                                  :date-picker/prev-day  "left"
+                                  :date-picker/next-day  "right"
+                                  :date-picker/prev-week ["up" "ctrl+p"]
+                                  :date-picker/next-week "down"}))
       (is (= @actual-ops 3))
-      (is (= (flatten-f (vals @target)) {:date-picker/complete         "tab"
-                                         :date-picker/prev-day         "left"
-                                         :date-picker/next-day         "right"
-                                         :date-picker/prev-week        ["up" "ctrl+p"]
-                                         :date-picker/next-week        "down"}))
+      (is (= (flatten-f @target) {:date-picker/complete  "tab"
+                                  :date-picker/prev-day  "left"
+                                  :date-picker/next-day  "right"
+                                  :date-picker/prev-week ["up" "ctrl+p"]
+                                  :date-picker/next-week "down"}))
       (is (= @actual-ops 3)))))
 
 (deftest test-media-format-from-input

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików