diff --git a/frontend/src/app/main/ui/settings/shortcuts.cljs b/frontend/src/app/main/ui/settings/shortcuts.cljs index afc90a9db5..bb1ba9d383 100644 --- a/frontend/src/app/main/ui/settings/shortcuts.cljs +++ b/frontend/src/app/main/ui/settings/shortcuts.cljs @@ -381,7 +381,8 @@ (mf/with-memo [] (fn [_ shortcut search-term] (or (str/blank? search-term) - (matches-search (:translation shortcut) search-term)))) + (matches-search (:translation shortcut) search-term) + (matches-search (ss/shortcut->command-string shortcut) search-term)))) filter-personalized (mf/use-fn @@ -395,7 +396,8 @@ (and customized? (not (false? (:customizable shortcut))) (or (str/blank? search-term) - (matches-search (:translation shortcut) search-term)))))) + (matches-search (:translation shortcut) search-term) + (matches-search (ss/shortcut->command-string shortcut) search-term)))))) filter-disabled (mf/use-fn @@ -409,7 +411,8 @@ (and in-group? blank? (not (false? (:customizable shortcut))) (or (str/blank? search-term) - (matches-search (:translation shortcut) search-term)))))) + (matches-search (:translation shortcut) search-term) + (matches-search (ss/shortcut->command-string shortcut) search-term)))))) on-import-file (mf/use-fn diff --git a/frontend/src/app/main/ui/shortcuts.cljs b/frontend/src/app/main/ui/shortcuts.cljs index cb223d0dc0..04d6aa77a9 100644 --- a/frontend/src/app/main/ui/shortcuts.cljs +++ b/frontend/src/app/main/ui/shortcuts.cljs @@ -162,6 +162,20 @@ [type item] (map (fn [[k v]] [k (assoc v :translation (translation-keyname type k))]) item)) +(defn shortcut->command-string + "Extract a lowercase searchable string from a shortcut entry's key combo(s). + Prefers `:show-command` (display override) over `:command` (Mousetrap format), + matching what the keycap UI renders. Joins vector commands (key sequences) + with a space so every token is searchable. Returns \"\" when there is no + command (e.g. a section/subsection node)." + [shortcut] + (let [cmd (or (:show-command shortcut) (:command shortcut))] + (-> (cond + (nil? cmd) "" + (vector? cmd) (str/join " " cmd) + :else (str cmd)) + (str/lower)))) + (defn shortcuts->subsections [shortcuts] (let [subsections (into #{} (mapcat :subsections) (vals shortcuts)) @@ -591,11 +605,19 @@ [{:keys [elements filter-term is-match-section is-match-subsection editable? custom-shortcuts section-key conflicts hidden subsection-name]}] (let [shortcut-translations (->> elements vals (map :translation) sort) - match-shortcut? (some #(matches-search % filter-term) shortcut-translations) + match-shortcut? (some (fn [info] + (or (matches-search (:translation info) filter-term) + (matches-search (shortcut->command-string info) filter-term))) + (vals elements)) filtered (if (and (or is-match-section is-match-subsection) (not match-shortcut?)) shortcut-translations - (filter #(matches-search % filter-term) shortcut-translations)) - sorted-filtered (sort filtered) + (->> (vals elements) + (filter (fn [info] + (or (matches-search (:translation info) filter-term) + (matches-search (shortcut->command-string info) filter-term)))) + (map :translation) + sort)) + sorted-filtered filtered trigger-ref (mf/use-ref nil)] [:ul {:class (stl/css :sub-menu) diff --git a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs index be454c585c..a21ef524ae 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs @@ -66,7 +66,12 @@ (ss/build-all-shortcuts workspace-shortcuts dashboard-shortcuts viewer-shortcuts) all-item-names (concat all-sc-names all-sub-names all-section-names) - match-any? (some #(matches-search % filter-term) all-item-names) + all-command-strings (->> (concat (vals workspace-shortcuts) + (vals dashboard-shortcuts) + (vals viewer-shortcuts)) + (map ss/shortcut->command-string)) + all-searchable-names (concat all-item-names all-command-strings) + match-any? (some #(matches-search % filter-term) all-searchable-names) manage-sections (fn [item] @@ -89,7 +94,8 @@ (fn [section term] (let [node-seq (tree-seq :children #(vals (:children %)) (get all-shortcuts section))] (reduce (fn [acc node] - (if (matches-search (:translation node) term) + (if (or (matches-search (:translation node) term) + (matches-search (ss/shortcut->command-string node) term)) (add-ids acc node) acc)) [] diff --git a/frontend/test/frontend_tests/ui/settings_shortcuts_test.cljs b/frontend/test/frontend_tests/ui/settings_shortcuts_test.cljs index 8bbf5af6b2..ba668650d7 100644 --- a/frontend/test/frontend_tests/ui/settings_shortcuts_test.cljs +++ b/frontend/test/frontend_tests/ui/settings_shortcuts_test.cljs @@ -5,6 +5,7 @@ [app.main.ui.settings.restore-shortcuts-modal :as restore-modal] [app.main.ui.settings.shortcuts :as sut] [app.main.ui.shortcuts :as ui-shortcuts] + [app.util.strings :refer [matches-search]] [cljs.test :as t :include-macros true] [clojure.string :as str])) @@ -221,3 +222,72 @@ (let [result (restore-modal/extract-shortcut-keys :next-frame {} :viewer)] (t/is (nth result 3) "Should return a default command for :next-frame in :viewer context"))) + +;; --- shortcut->command-string + command-based search -------------------- +;; The search in both the settings shortcuts page and the workspace sidebar +;; matches shortcut entries by their translated name AND by their key-combo +;; string. `shortcut->command-string` (in `app.main.ui.shortcuts`) extracts the +;; searchable form from `:command`/`:show-command`; `matches-search` does the +;; case-insensitive substring match. These tests pin that contract so searching +;; e.g. "ctrl" surfaces every shortcut whose combo includes ctrl. + +(t/deftest shortcut->command-string-extracts-string-command + (t/testing "a plain string command is returned lowercased" + (t/is (= "ctrl+z" (ui-shortcuts/shortcut->command-string + {:command "ctrl+z"}))))) + +(t/deftest shortcut->command-string-joins-vector-command + (t/testing "a vector command (key sequence) is joined with spaces so every + token is individually searchable" + (t/is (= "g v" (ui-shortcuts/shortcut->command-string + {:command ["g" "v"]}))))) + +(t/deftest shortcut->command-string-prefers-show-command + (t/testing ":show-command (display override) wins over :command" + (t/is (= "shift+x" (ui-shortcuts/shortcut->command-string + {:command "ctrl+z" :show-command "shift+x"}))))) + +(t/deftest shortcut->command-string-empty-for-section-node + (t/testing "a node without :command/:show-command (e.g. a section or + subsection heading) yields an empty string so it never matches a + non-blank command search" + (t/is (= "" (ui-shortcuts/shortcut->command-string + {:translation "workspace"}))))) + +(t/deftest shortcut->command-string-lowercases + (t/testing "the result is lowercased so search is case-insensitive" + (t/is (= "ctrl+shift+z" (ui-shortcuts/shortcut->command-string + {:command "Ctrl+Shift+Z"}))))) + +(t/deftest command-search-matches-ctrl-prefix + (t/testing "searching 'ctrl' matches a shortcut whose command contains ctrl" + (let [shortcut {:command "ctrl+shift+s" + :translation "Save all"}] + (t/is (matches-search (ui-shortcuts/shortcut->command-string shortcut) + "ctrl"))))) + +(t/deftest command-search-does-not-match-when-command-lacks-term + (t/testing "searching 'alt' does not match a shortcut with no alt in its combo" + (let [shortcut {:command "ctrl+z" + :translation "Undo"}] + (t/is (not (matches-search (ui-shortcuts/shortcut->command-string shortcut) + "alt")))))) + +(t/deftest command-search-matches-key-sequence-vector + (t/testing "searching a single key in a key-sequence vector command matches" + (let [shortcut {:command ["g" "v"] + :translation "Group"}] + (t/is (matches-search (ui-shortcuts/shortcut->command-string shortcut) + "g"))))) + +(t/deftest search-matches-by-translation-or-command + (t/testing "a search term matches if it appears in either the translation or + the command string — the OR that the filter predicates use" + (let [shortcut {:command "ctrl+s" + :translation "Save"}] + ;; by translation + (t/is (or (matches-search (:translation shortcut) "save") + (matches-search (ui-shortcuts/shortcut->command-string shortcut) "save"))) + ;; by command + (t/is (or (matches-search (:translation shortcut) "ctrl") + (matches-search (ui-shortcuts/shortcut->command-string shortcut) "ctrl"))))))