From 85d712cdad1c5bde6e6d2db07c7b93bee6222558 Mon Sep 17 00:00:00 2001 From: Gabriel Horner Date: Wed, 8 Apr 2026 14:30:45 -0400 Subject: [PATCH] enhance(cli): :values completion handles whitespace Any value in :values or :validate cli specs didn't autocomplete correctly. For example, for the --status option "in review" autocompleted as two separate entries. Given that other completion like :pages, :tags and :properties handled whitespaced completion, it seems reasonable for this to work for configured :values completion --- src/main/logseq/cli/completion_generator.cljs | 86 ++++++++++++++++--- .../logseq/cli/completion_generator_test.cljs | 37 ++++++++ 2 files changed, 110 insertions(+), 13 deletions(-) diff --git a/src/main/logseq/cli/completion_generator.cljs b/src/main/logseq/cli/completion_generator.cljs index 101309acd2..0f445c18bf 100644 --- a/src/main/logseq/cli/completion_generator.cljs +++ b/src/main/logseq/cli/completion_generator.cljs @@ -70,6 +70,40 @@ [spec] (mapv spec->token spec)) +;; --------------------------------------------------------------------------- +;; Value-quoting helpers (handle whitespace in :values entries) +;; --------------------------------------------------------------------------- + +(defn- contains-whitespace? + "Return true if any value in `values` contains whitespace." + [values] + (boolean (some #(re-find #"\s" %) values))) + +(defn- zsh-paren-value + "Backslash-escape whitespace in `v` so it stays a single token inside a zsh + `_arguments` parenthesized action like `(item1 item2 ...)`. Values without + whitespace are returned unchanged." + [v] + (string/replace v #"\s" #(str "\\" %))) + +(defn- zsh-shell-value + "Wrap `v` in double quotes if it contains whitespace, so the value survives + shell word-splitting inside a zsh `{shell-cmd}` action. Values without + whitespace are returned unchanged." + [v] + (if (re-find #"\s" v) + (str "\"" v "\"") + v)) + +(defn- bash-quote-value + "Single-quote `v` for bash if it contains whitespace, escaping any embedded + single quotes via the standard `'\\''` idiom. Values without whitespace are + returned unchanged." + [v] + (if (re-find #"\s" v) + (str "'" (string/replace v "'" "'\\''") "'") + v)) + ;; --------------------------------------------------------------------------- ;; Zsh dynamic helpers (verbatim preamble) ;; --------------------------------------------------------------------------- @@ -212,7 +246,7 @@ _logseq_multi_values() { (string/replace "]" "\\]") (string/replace "'" "'\\''"))) -(defn- zsh-token-for +(defn zsh-token-for "Generate zsh _arguments token strings for a spec token descriptor. Returns a vector of one or two strings. Aliased value-taking options produce separate long (--opt=) and short (-o+) specs so that -o=val is not suggested @@ -237,14 +271,14 @@ _logseq_multi_values() { (str "'" long-opt "[" desc* "]'"))])) :enum - (let [vals-str (string/join " " values)] + (let [vals-str (->> values (map zsh-paren-value) (string/join " "))] (if alias [(str "'" excl long-opt "=[" desc* "]:value:(" vals-str ")'") (str "'" excl alias-short "[" desc* "]:value:(" vals-str ")'")] [(str "'" long-opt "=[" desc* "]:value:(" vals-str ")'")])) :multi - (let [vals-str (string/join " " values)] + (let [vals-str (->> values (map zsh-shell-value) (string/join " "))] (if alias [(str "'" excl long-opt "=[" desc* "]:value:{_logseq_multi_values " vals-str "}'") (str "'" excl alias-short "[" desc* "]:value:{_logseq_multi_values " vals-str "}'")] @@ -552,6 +586,19 @@ _logseq_compadd_lines() { done < <(\"$source_fn\" \"$@\") } +_logseq_enum_values_bash() { + # Complete a fixed value list, preserving values that contain whitespace. + # Usage: _logseq_enum_values_bash \"$cur\" val1 val2 ... + local cur=\"$1\"; shift + COMPREPLY=() + local v + for v in \"$@\"; do + if [[ \"$v\" == \"$cur\"* ]]; then + COMPREPLY+=( \"$v\" ) + fi + done +} + _logseq_multi_values_bash() { # Complete comma-delimited lists. Usage: _logseq_multi_values_bash \"$cur\" val1 val2 ... local cur=\"$1\"; shift @@ -726,7 +773,7 @@ _logseq_multi_values_bash() { " printf '%s' \"$opts\"\n" "}\n"))) -(defn- bash-prev-completion-case +(defn bash-prev-completion-case "Generate a case branch for prev-word value completion." [{:keys [key type alias values complete]}] (let [long-opt (bash-option-name key) @@ -735,13 +782,19 @@ _logseq_multi_values_bash() { long-opt)] (case type :enum - (str " " pattern ")\n" - " COMPREPLY=( $(compgen -W '" (string/join " " values) "' -- \"$cur\") )\n" - " return ;;") + (if (contains-whitespace? values) + (str " " pattern ")\n" + " _logseq_enum_values_bash \"$cur\" " + (->> values (map bash-quote-value) (string/join " ")) "\n" + " return ;;") + (str " " pattern ")\n" + " COMPREPLY=( $(compgen -W '" (string/join " " values) "' -- \"$cur\") )\n" + " return ;;")) :multi (str " " pattern ")\n" - " _logseq_multi_values_bash \"$cur\" " (string/join " " values) "\n" + " _logseq_multi_values_bash \"$cur\" " + (->> values (map bash-quote-value) (string/join " ")) "\n" " return ;;") :dynamic @@ -843,10 +896,16 @@ _logseq_multi_values_bash() { (str "[[ \"$__cmd\" == '" cmd "' ]]"))] (case (:type token) :enum - (str " if " condition "; then\n" - " COMPREPLY=( $(compgen -W '" (string/join " " (:values token)) "' -- \"$cur\") )\n" - " return\n" - " fi") + (if (contains-whitespace? (:values token)) + (str " if " condition "; then\n" + " _logseq_enum_values_bash \"$cur\" " + (->> (:values token) (map bash-quote-value) (string/join " ")) "\n" + " return\n" + " fi") + (str " if " condition "; then\n" + " COMPREPLY=( $(compgen -W '" (string/join " " (:values token)) "' -- \"$cur\") )\n" + " return\n" + " fi")) :dynamic (case (:complete token) @@ -894,7 +953,8 @@ _logseq_multi_values_bash() { :multi (str " if " condition "; then\n" - " _logseq_multi_values_bash \"$cur\" " (string/join " " (:values token)) "\n" + " _logseq_multi_values_bash \"$cur\" " + (->> (:values token) (map bash-quote-value) (string/join " ")) "\n" " return\n" " fi") diff --git a/src/test/logseq/cli/completion_generator_test.cljs b/src/test/logseq/cli/completion_generator_test.cljs index f3ed9b3e86..9bf8d62b94 100644 --- a/src/test/logseq/cli/completion_generator_test.cljs +++ b/src/test/logseq/cli/completion_generator_test.cljs @@ -360,6 +360,43 @@ (testing "--fields case calls _logseq_multi_values_bash for list tag context" (is (string/includes? output "_logseq_multi_values_bash \"$cur\" created-at"))))) +(deftest test-values-with-whitespace + (testing "zsh enum action escapes whitespace inside parenthesized list" + (let [token {:key :status :type :enum :desc "Filter status" + :values ["backlog" "in review" "todo"]} + [spec] (gen/zsh-token-for token #{})] + (is (string/includes? spec "(backlog in\\ review todo)")))) + (testing "zsh multi action quotes whitespace values for shell call" + (let [token {:key :tags :type :multi :desc "Tags" + :values ["alpha" "two words" "beta"]} + [spec] (gen/zsh-token-for token #{})] + (is (string/includes? spec "{_logseq_multi_values alpha \"two words\" beta}")))) + (testing "bash enum branch with whitespace uses _logseq_enum_values_bash helper" + (let [token {:key :status :type :enum :desc "Filter status" + :values ["backlog" "in review" "todo"]} + branch (gen/bash-prev-completion-case token)] + (is (string/includes? branch "_logseq_enum_values_bash \"$cur\" backlog 'in review' todo")) + (is (not (string/includes? branch "compgen -W"))))) + (testing "bash enum branch without whitespace keeps compgen -W form" + (let [token {:key :order :type :enum :desc "Order" + :values ["asc" "desc"]} + branch (gen/bash-prev-completion-case token)] + (is (string/includes? branch "compgen -W 'asc desc'")) + (is (not (string/includes? branch "_logseq_enum_values_bash"))))) + (testing "bash multi branch with whitespace single-quotes affected values" + (let [token {:key :fields :type :multi :desc "Fields" + :values ["alpha" "two words" "beta"]} + branch (gen/bash-prev-completion-case token)] + (is (string/includes? branch "_logseq_multi_values_bash \"$cur\" alpha 'two words' beta")))) + (testing "single quote in a whitespace value is escaped via the standard '\\'' idiom" + (let [token {:key :tags :type :enum :desc "Tags" + :values ["it's a test"]} + branch (gen/bash-prev-completion-case token)] + (is (string/includes? branch "'it'\\''s a test'")))) + (testing "bash preamble defines _logseq_enum_values_bash helper" + (let [output (gen/generate-completions "bash" full-table)] + (is (string/includes? output "_logseq_enum_values_bash()"))))) + (deftest test-zsh-all-commands-present (let [output (gen/generate-completions "zsh" full-table)] (testing "every command from the table appears"