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
This commit is contained in:
Gabriel Horner
2026-04-08 14:30:45 -04:00
parent cc185452b0
commit 85d712cdad
2 changed files with 110 additions and 13 deletions

View File

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

View File

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