diff --git a/CHANGELOG.md b/CHANGELOG.md index 859ff73..96251e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - Add a dedicated mode for editing Joker code. (`clojure-ts-joker-mode`). - [#113](https://github.com/clojure-emacs/clojure-ts-mode/pull/113): Fix non-working refactoring commands for Emacs-30. +- [#114](https://github.com/clojure-emacs/clojure-ts-mode/pull/114): Extend built-in completion to complete keywords and local bindings in + `for` and `doseq` forms. ## 0.5.1 (2025-06-17) diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el index 9677f47..4802d9e 100644 --- a/clojure-ts-mode.el +++ b/clojure-ts-mode.el @@ -2592,6 +2592,10 @@ before DELIM-OPEN." :anchor ((sym_lit) @defun-candidate))))) "Query that matches top-level definitions.") +(defconst clojure-ts--completion-query-keywords + (treesit-query-compile 'clojure '((kwd_lit) @keyword-candidate)) + "Query that matches any Clojure keyword.") + (defconst clojure-ts--completion-defn-with-args-sym-regex (rx bol (or "defn" @@ -2613,7 +2617,9 @@ before DELIM-OPEN." "loop" "with-open" "dotimes" - "with-local-vars") + "with-local-vars" + "for" + "doseq") eol) "Regexp that matches a symbol of let-like form.") @@ -2627,7 +2633,8 @@ bindings vector as well as destructuring syntax.") (defconst clojure-ts--completion-annotations (list 'defun-candidate " Definition" - 'local-candidate " Local variable") + 'local-candidate " Local variable" + 'keyword-candidate " Keyword") "Property list of completion candidate type and annotation string.") (defun clojure-ts--completion-annotation-function (candidate) @@ -2652,9 +2659,9 @@ all functions along the way." (when-let* ((args-vec (clojure-ts--node-child parent-defun "vec_lit"))) (setq captured-nodes (append captured-nodes - (treesit-query-capture args-vec clojure-ts--completion-locals-query)) - parent-defun (treesit-parent-until parent-defun - #'clojure-ts--completion-defun-with-args-node-p)))) + (treesit-query-capture args-vec clojure-ts--completion-locals-query)))) + (setq parent-defun (treesit-parent-until parent-defun + #'clojure-ts--completion-defun-with-args-node-p))) captured-nodes)) (defun clojure-ts--completion-let-like-node-p (node) @@ -2673,9 +2680,9 @@ all let bindings found along the way." (when-let* ((bindings-vec (clojure-ts--node-child parent-let "vec_lit"))) (setq captured-nodes (append captured-nodes - (treesit-query-capture bindings-vec clojure-ts--completion-locals-query)) - parent-let (treesit-parent-until parent-let - #'clojure-ts--completion-let-like-node-p)))) + (treesit-query-capture bindings-vec clojure-ts--completion-locals-query)))) + (setq parent-let (treesit-parent-until parent-let + #'clojure-ts--completion-let-like-node-p))) captured-nodes)) (defun clojure-ts-completion-at-point-function () @@ -2683,6 +2690,7 @@ all let bindings found along the way." (when-let* ((bounds (bounds-of-thing-at-point 'symbol)) (source (treesit-buffer-root-node 'clojure)) (nodes (append (treesit-query-capture source clojure-ts--completion-query-defuns) + (treesit-query-capture source clojure-ts--completion-query-keywords) (clojure-ts--completion-fn-args-nodes) (clojure-ts--completion-let-locals-nodes)))) (list (car bounds) @@ -2692,7 +2700,7 @@ all let bindings found along the way." (seq-remove (lambda (item) (= (treesit-node-end (cdr item)) (point)))) ;; Remove unwanted captured nodes (seq-filter (lambda (item) - (not (member (car item) '(sym kwd))))) + (not (equal (car item) 'sym)))) ;; Produce alist of candidates (seq-map (lambda (item) (cons (treesit-node-text (cdr item) t) (car item)))) ;; Remove duplicated candidates diff --git a/test/clojure-ts-mode-completion.el b/test/clojure-ts-mode-completion.el index 1bc92ce..ffa30df 100644 --- a/test/clojure-ts-mode-completion.el +++ b/test/clojure-ts-mode-completion.el @@ -46,7 +46,9 @@ b|" (expect (nth 2 (clojure-ts-completion-at-point-function)) :to-equal '(("foo" . defun-candidate) ("bar" . defun-candidate) - ("baz" . defun-candidate))))) + ("baz" . defun-candidate) + (":first" . keyword-candidate) + (":second" . keyword-candidate))))) (it "should complete function arguments" (with-clojure-ts-buffer-point " @@ -61,6 +63,8 @@ b|" :to-equal '(("foo" . defun-candidate) ("bar" . defun-candidate) ("baz" . defun-candidate) + (":first" . keyword-candidate) + (":second" . keyword-candidate) ("username" . local-candidate))))) (it "should not complete function arguments outside of function" @@ -77,7 +81,9 @@ u|" (expect (nth 2 (clojure-ts-completion-at-point-function)) :to-equal '(("foo" . defun-candidate) ("bar" . defun-candidate) - ("baz" . defun-candidate))))) + ("baz" . defun-candidate) + (":first" . keyword-candidate) + (":second" . keyword-candidate))))) (it "should complete destructured function arguments" (with-clojure-ts-buffer-point " @@ -86,6 +92,7 @@ u|" (println u|))" (expect (nth 2 (clojure-ts-completion-at-point-function)) :to-equal '(("baz" . defun-candidate) + (":keys" . keyword-candidate) ("username" . local-candidate)))) (with-clojure-ts-buffer-point " @@ -94,6 +101,7 @@ u|" (println u|))" (expect (nth 2 (clojure-ts-completion-at-point-function)) :to-equal '(("baz" . defun-candidate) + (":strs" . keyword-candidate) ("username" . local-candidate)))) (with-clojure-ts-buffer-point " @@ -102,6 +110,7 @@ u|" (println u|))" (expect (nth 2 (clojure-ts-completion-at-point-function)) :to-equal '(("baz" . defun-candidate) + (":syms" . keyword-candidate) ("username" . local-candidate)))) (with-clojure-ts-buffer-point " @@ -110,6 +119,7 @@ u|" (println u|))" (expect (nth 2 (clojure-ts-completion-at-point-function)) :to-equal '(("baz" . defun-candidate) + (":name" . keyword-candidate) ("username" . local-candidate)))) (with-clojure-ts-buffer-point " @@ -131,6 +141,9 @@ u|" a|))" (expect (nth 2 (clojure-ts-completion-at-point-function)) :to-equal '(("baz" . defun-candidate) + (":street" . keyword-candidate) + (":zip-code" . keyword-candidate) + (":keys" . keyword-candidate) ("first-name" . local-candidate) ("last-name" . local-candidate) ("address" . local-candidate) @@ -147,7 +160,43 @@ u|" (expect (nth 2 (clojure-ts-completion-at-point-function)) :to-equal '(("baz" . defun-candidate) ("first-name" . local-candidate) - ("full-name" . local-candidate)))))) + ("full-name" . local-candidate))))) + + (it "should complete any keyword" + (with-clojure-ts-buffer-point " +(defn baz + [first-name] + (let [last-name \"Doe\" + address {:street \"Whatever\" :zip-code 2222} + {:keys [street zip-code]} address] + (println street zip-code))) + +:|" + (expect (nth 2 (clojure-ts-completion-at-point-function)) + :to-equal '(("baz" . defun-candidate) + (":street" . keyword-candidate) + (":zip-code" . keyword-candidate) + (":keys" . keyword-candidate))))) + + (it "should complete locals of for bindings" + (with-clojure-ts-buffer-point " +(for [digit [\"one\" \"two\" \"three\"] + :let [prefixed-digit (str \"hello-\" digit)]] + (println d|))" + (expect (nth 2 (clojure-ts-completion-at-point-function)) + :to-equal '((":let" . keyword-candidate) + ("digit" . local-candidate) + ("prefixed-digit" . local-candidate))))) + + (it "should complete locals of doseq bindings" + (with-clojure-ts-buffer-point " +(doseq [digit [\"one\" \"two\" \"three\"] + :let [prefixed-digit (str \"hello-\" digit)]] + (println d|))" + (expect (nth 2 (clojure-ts-completion-at-point-function)) + :to-equal '((":let" . keyword-candidate) + ("digit" . local-candidate) + ("prefixed-digit" . local-candidate)))))) (provide 'clojure-ts-mode-completion) ;;; clojure-ts-mode-completion.el ends here diff --git a/test/samples/completion.clj b/test/samples/completion.clj index 16b64de..7207d7f 100644 --- a/test/samples/completion.clj +++ b/test/samples/completion.clj @@ -54,3 +54,18 @@ ;; Both arguments are available here. (= item top-arg)) [1 2 3 4 5])) + +;; Works for top-level bindings and for nested `:let` bindings. +(for [digit vec-variable + :let [prefixed-digit (str "hello-" digit)]] + (println prefixed-digit digit)) + +;; Same for `doseq` +(doseq [word vec-variable + :let [suffixed-word (str "hello-" word)]] + (println suffixed-word word)) + +;; Can complete any keyword from the buffer +(do :users/usename + :address + :kwd)