Feat/support keybinding for plugin commands (#3176)

register shortcut and shortcut for palette commands
This commit is contained in:
Charlie
2021-11-22 15:44:27 +08:00
committed by GitHub
parent e654fde59d
commit c2b882b57d
7 changed files with 219 additions and 144 deletions

View File

@@ -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

View File

@@ -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)
},

View File

@@ -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)]

View File

@@ -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))
(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)))))
(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))
(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))))))
_ (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 "reloaded"
(fn [^js pl]
(register-plugin
(bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
(.on "unregistered" (fn [pid]
(let [pid (keyword pid)]
(.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))))
(clear-commands! pid))))
(.on "unlink-plugin" (fn [pid]
(let [pid (keyword pid)]
(ipc/ipc "uninstallMarketPlugin" (name 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 "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 "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-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 "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)))))))
(.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)
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))))
_ (.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"

View File

@@ -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)]
(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))
(when shortcut-map
(shortcut-config/add-shortcut! handler-id id shortcut-map))
;; handler
handler-id)]
(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))))))))
(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)

View File

@@ -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})))))))

View File

@@ -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]