Pārlūkot izejas kodu

Feat/support keybinding for plugin commands (#3176)

register shortcut and shortcut for palette commands
Charlie 4 gadi atpakaļ
vecāks
revīzija
c2b882b57d

+ 9 - 1
libs/src/LSPlugin.ts

@@ -179,6 +179,12 @@ export type SimpleCommandCallback = (e: IHookEvent) => void
 export type BlockCommandCallback = (e: IHookEvent & { uuid: BlockUUID }) => Promise<void>
 export type BlockCursorPosition = { left: number, top: number, height: number, pos: number, rect: DOMRect }
 
+export type SimpleCommandKeybinding = {
+  mode?: 'global' | 'non-editing' | 'editing',
+  binding: string,
+  mac?: string // special for Mac OS
+}
+
 /**
  * App level APIs
  */
@@ -194,7 +200,8 @@ export interface IAppProxy {
       key: string,
       label: string,
       desc?: string,
-      palette?: boolean
+      palette?: boolean,
+      keybinding?: SimpleCommandKeybinding
     },
     action: SimpleCommandCallback) => void
 
@@ -202,6 +209,7 @@ export interface IAppProxy {
     opts: {
       key: string,
       label: string,
+      keybinding?: SimpleCommandKeybinding
     },
     action: SimpleCommandCallback) => void
 

+ 8 - 7
libs/src/LSPlugin.user.ts

@@ -12,7 +12,7 @@ import {
   ThemeOptions,
   UIOptions, IHookEvent, BlockIdentity,
   BlockPageName,
-  UIContainerAttrs, SimpleCommandCallback
+  UIContainerAttrs, SimpleCommandCallback, SimpleCommandKeybinding
 } from './LSPlugin'
 import Debug from 'debug'
 import * as CSS from 'csstype'
@@ -41,7 +41,8 @@ function registerSimpleCommand (
     key: string,
     label: string,
     desc?: string,
-    palette?: boolean
+    palette?: boolean,
+    keybinding?: SimpleCommandKeybinding
   },
   action: SimpleCommandCallback
 ) {
@@ -49,14 +50,14 @@ function registerSimpleCommand (
     return false
   }
 
-  const { key, label, desc, palette } = opts
+  const { key, label, desc, palette, keybinding } = opts
   const eventKey = `SimpleCommandHook${key}${++registeredCmdUid}`
 
   this.Editor['on' + eventKey](action)
 
   this.caller?.call(`api:call`, {
     method: 'register-plugin-simple-command',
-    args: [this.baseInfo.id, [{ key, label, type, desc }, ['editor/hook', eventKey]], palette]
+    args: [this.baseInfo.id, [{ key, label, type, desc, keybinding }, ['editor/hook', eventKey]], palette]
   })
 }
 
@@ -64,15 +65,15 @@ const app: Partial<IAppProxy> = {
   registerCommand: registerSimpleCommand,
 
   registerCommandPalette (
-    opts: { key: string; label: string },
+    opts: { key: string; label: string, keybinding?: SimpleCommandKeybinding },
     action: SimpleCommandCallback) {
 
-    const { key, label } = opts
+    const { key, label, keybinding } = opts
     const group = 'global-palette-command'
 
     return registerSimpleCommand.call(
       this, group,
-      { key, label, palette: true },
+      { key, label, palette: true, keybinding },
       action)
   },
 

+ 6 - 0
src/main/frontend/handler/events.cljs

@@ -20,6 +20,7 @@
             [frontend.handler.notification :as notification]
             [frontend.handler.page :as page-handler]
             [frontend.handler.ui :as ui-handler]
+            [frontend.modules.shortcut.core :as st]
             [frontend.commands :as commands]
             [frontend.spec :as spec]
             [frontend.state :as state]
@@ -215,6 +216,11 @@
 (defmethod handle :exec-plugin-cmd [[_ {:keys [type key pid cmd action]}]]
   (commands/exec-plugin-simple-command! pid cmd action))
 
+(defmethod handle :shortcut-handler-refreshed [[_]]
+  (when-not @st/*inited?
+    (reset! st/*inited? true)
+    (st/consume-pending-shortcuts!)))
+
 (defn run!
   []
   (let [chan (state/get-events-chan)]

+ 143 - 121
src/main/frontend/handler/plugin.cljs

@@ -18,8 +18,8 @@
             [frontend.format :as format]))
 
 (defonce lsp-enabled?
-         (and (util/electron?)
-              (= (storage/get "developer-mode") "true")))
+  (and (util/electron?)
+       (= (storage/get "developer-mode") "true")))
 
 (defn invoke-exported-api
   [type & args]
@@ -45,26 +45,26 @@
   [refresh?]
   (if (or refresh? (nil? (:plugin/marketplace-pkgs @state/state)))
     (p/create
-      (fn [resolve reject]
-        (-> (util/fetch plugins-url
-                        (fn [res]
-                          (let [pkgs (:packages res)]
-                            (state/set-state! :plugin/marketplace-pkgs pkgs)
-                            (resolve pkgs)))
-                        reject)
-            (p/catch reject))))
+     (fn [resolve reject]
+       (-> (util/fetch plugins-url
+                       (fn [res]
+                         (let [pkgs (:packages res)]
+                           (state/set-state! :plugin/marketplace-pkgs pkgs)
+                           (resolve pkgs)))
+                       reject)
+           (p/catch reject))))
     (p/resolved (:plugin/marketplace-pkgs @state/state))))
 
 (defn load-marketplace-stats
   [refresh?]
   (if (or refresh? (nil? (:plugin/marketplace-stats @state/state)))
     (p/create
-      (fn [resolve reject]
-        (util/fetch stats-url
-                    (fn [res]
-                      (state/set-state! :plugin/marketplace-stats res)
-                      (resolve nil))
-                    reject)))
+     (fn [resolve reject]
+       (util/fetch stats-url
+                   (fn [res]
+                     (state/set-state! :plugin/marketplace-stats res)
+                     (resolve nil))
+                   reject)))
     (p/resolved nil)))
 
 (defn installed?
@@ -77,30 +77,30 @@
   (when-not (and (:plugin/installing @state/state)
                  (installed? id))
     (p/create
-      (fn [resolve]
-        (state/set-state! :plugin/installing mft)
-        (ipc/ipc "installMarketPlugin" mft)
-        (resolve id)))))
+     (fn [resolve]
+       (state/set-state! :plugin/installing mft)
+       (ipc/ipc "installMarketPlugin" mft)
+       (resolve id)))))
 
 (defn update-marketplace-plugin
   [{:keys [id] :as pkg} error-handler]
   (when-not (and (:plugin/installing @state/state)
                  (not (installed? id)))
     (p/catch
-      (p/then
-        (do (state/set-state! :plugin/installing pkg)
-            (load-marketplace-plugins false))
-        (fn [mfts]
-          (if-let [mft (some #(if (= (:id %) id) %) mfts)]
-            (do
-              (ipc/ipc "updateMarketPlugin" (merge (dissoc pkg :logger) mft)))
-            (throw (js/Error. (str ":central-not-matched " id))))
-          true))
-
-      (fn [^js e]
-        (error-handler "Update Error: remote error")
-        (state/set-state! :plugin/installing nil)
-        (js/console.error e)))))
+     (p/then
+      (do (state/set-state! :plugin/installing pkg)
+          (load-marketplace-plugins false))
+      (fn [mfts]
+        (if-let [mft (some #(if (= (:id %) id) %) mfts)]
+          (do
+            (ipc/ipc "updateMarketPlugin" (merge (dissoc pkg :logger) mft)))
+          (throw (js/Error. (str ":central-not-matched " id))))
+        true))
+
+     (fn [^js e]
+       (error-handler "Update Error: remote error")
+       (state/set-state! :plugin/installing nil)
+       (js/console.error e)))))
 
 (defn get-plugin-inst
   [id]
@@ -124,18 +124,18 @@
                          (if (installed? id)
                            (when-let [^js pl (get-plugin-inst id)] ;; update
                              (p/then
-                               (.reload pl)
-                               #(do
+                              (.reload pl)
+                              #(do
                                   ;;(if theme (select-a-plugin-theme id))
-                                  (notifications/show!
-                                    (str (t :plugin/update) (t :plugins) ": " name " - " (.-version (.-options pl))) :success))))
+                                 (notifications/show!
+                                  (str (t :plugin/update) (t :plugins) ": " name " - " (.-version (.-options pl))) :success))))
 
                            (do                              ;; register new
                              (p/then
-                               (js/LSPluginCore.register (bean/->js {:key id :url dst}))
-                               (fn [] (if theme (js/setTimeout #(select-a-plugin-theme id) 300))))
+                              (js/LSPluginCore.register (bean/->js {:key id :url dst}))
+                              (fn [] (if theme (js/setTimeout #(select-a-plugin-theme id) 300))))
                              (notifications/show!
-                               (str (t :plugin/installed) (t :plugins) ": " name) :success))))
+                              (str (t :plugin/installed) (t :plugins) ": " name) :success))))
 
                        :error
                        (let [[msg type] (case (keyword (string/replace payload #"^[\s\:]+" ""))
@@ -146,9 +146,9 @@
                                           [payload :error])]
 
                          (notifications/show!
-                           (str
-                             (if (= :error type) "[Install Error]" "")
-                             msg) type)
+                          (str
+                           (if (= :error type) "[Install Error]" "")
+                           msg) type)
 
                          (js/console.error payload))
 
@@ -188,15 +188,37 @@
   [pid]
   (swap! state/state md/dissoc-in [:plugin/installed-commands (keyword pid)]))
 
+(def keybinding-mode-handler-map
+  {:global      :shortcut.handler/editor-global
+   :non-editing :shortcut.handler/global-non-editing-only
+   :editing     :shortcut.handler/block-editing-only})
+
 (defn simple-cmd->palette-cmd
-  [pid {:keys [key label type desc] :as cmd} action]
-  (let [palette-cmd {:id     (keyword (str "plugin." pid "/" key))
-                     :desc   (or desc label)
-                     :action (fn []
-                               (state/pub-event!
-                                 [:exec-plugin-cmd {:type type :key key :pid pid :cmd cmd :action action}]))}]
+  [pid {:keys [key label type desc keybinding] :as cmd} action]
+  (let [palette-cmd {:id       (keyword (str "plugin." pid "/" key))
+                     :desc     (or desc label)
+                     :shortcut (when-let [shortcut (:binding keybinding)]
+                                 (if util/mac?
+                                   (or (:mac keybinding) shortcut)
+                                   shortcut))
+                     :handler-id (let [mode (or (:mode keybinding) :global)]
+                                   (get keybinding-mode-handler-map (keyword mode)))
+                     :action   (fn []
+                                 (state/pub-event!
+                                  [:exec-plugin-cmd {:type type :key key :pid pid :cmd cmd :action action}]))}]
     palette-cmd))
 
+(defn simple-cmd-keybinding->shortcut-args
+  [pid key keybinding]
+  (let [id (keyword (str "plugin." pid "/" key))
+        binding (:binding keybinding)
+        binding (if util/mac?
+                  (or (:mac keybinding) binding)
+                  binding)
+        mode (or (:mode keybinding) :global)
+        mode (get keybinding-mode-handler-map (keyword mode))]
+    [mode id {:binding binding}]))
+
 (defn register-plugin-simple-command
   ;; action => [:action-key :event-key]
   [pid {:keys [key label type] :as cmd} action]
@@ -245,11 +267,11 @@
     (when-not (string/blank? content)
       (let [content (if-not (string/blank? url)
                       (string/replace
-                        content #"!\[[^\]]*\]\((.*?)\s*(\"(?:.*[^\"])\")?\s*\)"
-                        (fn [[matched link]]
-                          (if (and link (not (string/starts-with? link "http")))
-                            (string/replace matched link (util/node-path.join url link))
-                            matched)))
+                       content #"!\[[^\]]*\]\((.*?)\s*(\"(?:.*[^\"])\")?\s*\)"
+                       (fn [[matched link]]
+                         (if (and link (not (string/starts-with? link "http")))
+                           (string/replace matched link (util/node-path.join url link))
+                           matched)))
                       content)]
         (format/to-html content :markdown (mldoc/default-config :markdown))))
     (catch js/Error e
@@ -316,11 +338,11 @@
 (defn- get-user-default-plugins
   []
   (p/catch
-    (p/let [files ^js (ipc/ipc "getUserDefaultPlugins")
-            files (js->clj files)]
-      (map #(hash-map :url %) files))
-    (fn [e]
-      (js/console.error e))))
+   (p/let [files ^js (ipc/ipc "getUserDefaultPlugins")
+           files (js->clj files)]
+     (map #(hash-map :url %) files))
+   (fn [e]
+     (js/console.error e))))
 
 ;; components
 (rum/defc lsp-indicator < rum/reactive
@@ -338,76 +360,76 @@
   (let [el (js/document.createElement "div")]
     (.appendChild js/document.body el)
     (rum/mount
-      (lsp-indicator) el))
+     (lsp-indicator) el))
 
   (state/set-state! :plugin/indicator-text "LOADING")
 
   (p/then
-    (p/let [root (get-ls-dotdir-root)
-            _ (.setupPluginCore js/LSPlugin (bean/->js {:localUserConfigRoot root :dotConfigRoot root}))
+   (p/let [root (get-ls-dotdir-root)
+           _ (.setupPluginCore js/LSPlugin (bean/->js {:localUserConfigRoot root :dotConfigRoot root}))
 
-            clear-commands! (fn [pid]
+           clear-commands! (fn [pid]
                               ;; commands
-                              (unregister-plugin-slash-command pid)
-                              (invoke-exported-api "unregister_plugin_simple_command" pid)
-                              (unregister-plugin-ui-items pid))
-
-            _ (doto js/LSPluginCore
-                (.on "registered"
-                     (fn [^js pl]
-                       (register-plugin
-                         (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
-
-                (.on "reloaded"
-                     (fn [^js pl]
-                       (register-plugin
-                         (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
-
-                (.on "unregistered" (fn [pid]
-                                      (let [pid (keyword pid)]
+                             (unregister-plugin-slash-command pid)
+                             (invoke-exported-api "unregister_plugin_simple_command" pid)
+                             (unregister-plugin-ui-items pid))
+
+           _ (doto js/LSPluginCore
+               (.on "registered"
+                    (fn [^js pl]
+                      (register-plugin
+                       (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
+
+               (.on "reloaded"
+                    (fn [^js pl]
+                      (register-plugin
+                       (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
+
+               (.on "unregistered" (fn [pid]
+                                     (let [pid (keyword pid)]
                                         ;; effects
-                                        (unregister-plugin-themes pid)
+                                       (unregister-plugin-themes pid)
                                         ;; plugins
-                                        (swap! state/state md/dissoc-in [:plugin/installed-plugins pid])
+                                       (swap! state/state md/dissoc-in [:plugin/installed-plugins pid])
                                         ;; commands
-                                        (clear-commands! pid))))
-
-                (.on "unlink-plugin" (fn [pid]
-                                       (let [pid (keyword pid)]
-                                         (ipc/ipc "uninstallMarketPlugin" (name pid)))))
-
-                (.on "beforereload" (fn [^js pl]
-                                      (let [pid (.-id pl)]
-                                        (clear-commands! pid)
-                                        (unregister-plugin-themes pid false))))
-
-                (.on "disabled" (fn [pid]
-                                  (clear-commands! pid)
-                                  (unregister-plugin-themes pid)))
-
-                (.on "theme-changed" (fn [^js themes]
-                                       (swap! state/state assoc :plugin/installed-themes
-                                              (vec (mapcat (fn [[pid vs]] (mapv #(assoc % :pid pid) (bean/->clj vs))) (bean/->clj themes))))))
-
-                (.on "theme-selected" (fn [^js opts]
-                                        (let [opts (bean/->clj opts)
-                                              url (:url opts)
-                                              mode (:mode opts)]
-                                          (when mode (state/set-theme! mode))
-                                          (state/set-state! :plugin/selected-theme url))))
-
-                (.on "settings-changed" (fn [id ^js settings]
-                                          (let [id (keyword id)]
-                                            (when (and settings
-                                                       (contains? (:plugin/installed-plugins @state/state) id))
-                                              (update-plugin-settings id (bean/->clj settings)))))))
-
-            default-plugins (get-user-default-plugins)
-
-            _ (.register js/LSPluginCore (bean/->js (if (seq default-plugins) default-plugins [])) true)])
-    #(do
-       (state/set-state! :plugin/indicator-text "END")
-       (callback))))
+                                       (clear-commands! pid))))
+
+               (.on "unlink-plugin" (fn [pid]
+                                      (let [pid (keyword pid)]
+                                        (ipc/ipc "uninstallMarketPlugin" (name pid)))))
+
+               (.on "beforereload" (fn [^js pl]
+                                     (let [pid (.-id pl)]
+                                       (clear-commands! pid)
+                                       (unregister-plugin-themes pid false))))
+
+               (.on "disabled" (fn [pid]
+                                 (clear-commands! pid)
+                                 (unregister-plugin-themes pid)))
+
+               (.on "theme-changed" (fn [^js themes]
+                                      (swap! state/state assoc :plugin/installed-themes
+                                             (vec (mapcat (fn [[pid vs]] (mapv #(assoc % :pid pid) (bean/->clj vs))) (bean/->clj themes))))))
+
+               (.on "theme-selected" (fn [^js opts]
+                                       (let [opts (bean/->clj opts)
+                                             url (:url opts)
+                                             mode (:mode opts)]
+                                         (when mode (state/set-theme! mode))
+                                         (state/set-state! :plugin/selected-theme url))))
+
+               (.on "settings-changed" (fn [id ^js settings]
+                                         (let [id (keyword id)]
+                                           (when (and settings
+                                                      (contains? (:plugin/installed-plugins @state/state) id))
+                                             (update-plugin-settings id (bean/->clj settings)))))))
+
+           default-plugins (get-user-default-plugins)
+
+           _ (.register js/LSPluginCore (bean/->js (if (seq default-plugins) default-plugins [])) true)])
+   #(do
+      (state/set-state! :plugin/indicator-text "END")
+      (callback))))
 
 (defn setup!
   "setup plugin core handler"

+ 39 - 24
src/main/frontend/modules/shortcut/core.cljs

@@ -14,6 +14,8 @@
            [goog.ui KeyboardShortcutHandler]))
 
 (def *installed (atom {}))
+(def *inited? (atom false))
+(def *pending (atom []))
 
 (def global-keys #js
   [KeyCodes/TAB
@@ -23,6 +25,15 @@
 
 (def key-names (js->clj KeyNames))
 
+(declare register-shortcut!)
+
+(defn consume-pending-shortcuts!
+  []
+  (when (and @*inited? (seq @*pending))
+    (doseq [[handler-id id shortcut] @*pending]
+      (register-shortcut! handler-id id shortcut))
+    (reset! *pending [])))
+
 (defn- get-handler-by-id
   [handler-id]
   (-> (filter #(= (:group %) handler-id) (vals @*installed))
@@ -39,25 +50,28 @@
   ([handler-id id]
    (register-shortcut! handler-id id nil))
   ([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))
-                        ;; handler
-                        handler-id)]
-
-     (when shortcut-map
-       (shortcut-config/add-shortcut! handler-id id shortcut-map))
-
-     (when-not (false? (dh/shortcut-binding id))
-       (doseq [k (dh/shortcut-binding id)]
-         (try
-           (log/debug :shortcut/register-shortcut {:id id :binding k})
-           (.registerShortcut handler (util/keyname id) k)
-           (catch js/Object e
-             (log/error :shortcut/register-shortcut {:id id
-                                                     :binding k
-                                                     :error e})
-             (notification/show! (str/join " " [id k (.-message e)]) :error false))))))))
+   (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))
+
+                          ;; handler
+                          handler-id)]
+
+       (when shortcut-map
+         (shortcut-config/add-shortcut! handler-id id shortcut-map))
+
+       (when-not (false? (dh/shortcut-binding id))
+         (doseq [k (dh/shortcut-binding id)]
+           (try
+             (log/debug :shortcut/register-shortcut {:id id :binding k})
+             (.registerShortcut handler (util/keyname id) k)
+             (catch js/Object e
+               (log/error :shortcut/register-shortcut {:id      id
+                                                       :binding k
+                                                       :error   e})
+               (notification/show! (str/join " " [id k (.-message e)]) :error false)))))))))
 
 (defn unregister-shortcut!
   "Unregister a shortcut.
@@ -65,10 +79,10 @@
   (unregister-shortcut! :shortcut.handler/misc :foo/bar)"
   [handler-id shortcut-id]
   (when-let [handler (get-handler-by-id handler-id)]
-    (when shortcut-id
-      (let [k (dh/shortcut-binding shortcut-id)]
-        (.unregisterShortcut ^js handler k))
-      (shortcut-config/remove-shortcut! handler-id shortcut-id))))
+    (when-let [ks (dh/shortcut-binding shortcut-id)]
+      (doseq [k ks]
+        (.unregisterShortcut ^js handler k)))
+    (shortcut-config/remove-shortcut! handler-id shortcut-id)))
 
 (defn uninstall-shortcut!
   [install-id]
@@ -168,7 +182,8 @@
   (log/info :shortcut/refresh @*installed)
   (doseq [id (keys @*installed)]
     (uninstall-shortcut! id))
-  (install-shortcuts!))
+  (install-shortcuts!)
+  (state/pub-event! [:shortcut-handler-refreshed]))
 
 (defn- name-with-meta [e]
   (let [ctrl    (.-ctrlKey e)

+ 1 - 1
src/main/frontend/modules/shortcut/data_helper.cljs

@@ -201,6 +201,6 @@
   (let [m (get @config/config handler-id)]
     (->> m
          (map (fn [[id _]] (-> (shortcut-data-by-id id)
-                               (assoc :id id)
+                               (assoc :id id :handler-id handler-id)
                                (rename-keys {:binding :shortcut
                                              :fn      :action})))))))

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

@@ -24,6 +24,7 @@
             [frontend.modules.outliner.core :as outliner]
             [frontend.modules.outliner.tree :as outliner-tree]
             [frontend.handler.command-palette :as palette-handler]
+            [frontend.modules.shortcut.core :as st]
             [electron.listener :as el]
             [frontend.state :as state]
             [frontend.util :as util]
@@ -231,19 +232,41 @@
 (def ^:export register_plugin_simple_command
   (fn [pid ^js cmd-action palette?]
     (when-let [[cmd action] (bean/->clj cmd-action)]
-      (let [action (assoc action 0 (keyword (first action)))]
+      (let [action (assoc action 0 (keyword (first action)))
+            key (:key cmd)
+            keybinding (:keybinding cmd)
+            palette-cmd (and palette? (plugin-handler/simple-cmd->palette-cmd pid cmd action))]
+
+        ;; handle simple commands
         (plugin-handler/register-plugin-simple-command pid cmd action)
-        (when-let [palette-cmd (and palette? (plugin-handler/simple-cmd->palette-cmd pid cmd action))]
-          (palette-handler/register palette-cmd))))))
+
+        ;; handle palette commands
+        (when palette-cmd
+          (palette-handler/register palette-cmd))
+
+        ;; handle keybinding commands
+        (when-let [shortcut-args (and palette-cmd keybinding
+                                      (plugin-handler/simple-cmd-keybinding->shortcut-args pid key keybinding))]
+          (let [dispatch-cmd (fn [_ e] (palette-handler/invoke-command palette-cmd))
+                [handler-id id shortcut-map] (update shortcut-args 2 assoc :fn dispatch-cmd)]
+            (js/console.debug :shortcut/register-shortcut [handler-id id shortcut-map])
+            (st/register-shortcut! handler-id id shortcut-map)))))))
 
 (defn ^:export unregister_plugin_simple_command
   [pid]
+  ;; remove simple commands
   (plugin-handler/unregister-plugin-simple-command pid)
+
+  ;; remove palette commands
   (let [palette-matched (->> (palette-handler/get-commands)
                              (filter #(string/includes? (str (:id %)) (str "plugin." pid))))]
     (when (seq palette-matched)
       (doseq [cmd palette-matched]
-        (palette-handler/unregister (:id cmd))))))
+        (palette-handler/unregister (:id cmd))
+        ;; remove keybinding commands
+        (when (seq (:shortcut cmd))
+          (js/console.debug :shortcut/unregister-shortcut cmd)
+          (st/unregister-shortcut! (:handler-id cmd) (:id cmd)))))))
 
 (def ^:export register_plugin_ui_item
   (fn [pid type ^js opts]