branch: elpa/clojure-ts-mode
commit 95aed261172c2d8133dd4c87c2d5cfcf2ea30476
Author: Roman Rudakov <[email protected]>
Commit: Bozhidar Batsov <[email protected]>
[#107] Introduce clojure-ts-completion-at-point-function
---
CHANGELOG.md | 3 +-
README.md | 20 +++--
clojure-ts-mode.el | 169 ++++++++++++++++++++++++++++++++-----
test/clojure-ts-mode-completion.el | 153 +++++++++++++++++++++++++++++++++
test/samples/completion.clj | 56 ++++++++++++
5 files changed, 376 insertions(+), 25 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2045f19363..15471275d6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,8 +15,9 @@
highlighting C++ syntax in Jank `native/raw` forms.
- [#103](https://github.com/clojure-emacs/clojure-ts-mode/issues/103):
Introduce `clojure-ts-clojurescript-use-js-parser` customization which
allows highlighting JS syntax in ClojureScript `js*` forms.
-- Introduce the `clojure-ts-extra-def-forms` customization option to specify
+- [#104](https://github.com/clojure-emacs/clojure-ts-mode/pull/104): Introduce
the `clojure-ts-extra-def-forms` customization option to specify
additional `defn`-like forms that should be fontified.
+- Introduce completion feature and `clojure-ts-completion-enabled`
customization.
## 0.4.0 (2025-05-15)
diff --git a/README.md b/README.md
index 703ea3a1f0..6a86762dd9 100644
--- a/README.md
+++ b/README.md
@@ -539,6 +539,21 @@ multi-arity function or macro. Function can be defined
using `defn`, `fn` or
By default prefix for all refactoring commands is `C-c C-r`. It can be changed
by customizing `clojure-ts-refactor-map-prefix` variable.
+## Code completion
+
+`clojure-ts-mode` provides basic code completion functionality. Completion
only
+works for the current source buffer and includes completion of top-level
+definitions and local bindings. This feature can be turned off by setting:
+
+```emacs-lisp
+(setopt clojure-ts-completion-enabled nil)
+```
+
+Here's the short video illustrating the feature with built-in completion (it
+should also work well with more advanced packages like company and corfu):
+
+https://github.com/user-attachments/assets/7c37179f-5a5d-424f-9bd6-9c8525f6b2f7
+
## Migrating to clojure-ts-mode
If you are migrating to `clojure-ts-mode` note that `clojure-mode` is still
@@ -576,11 +591,6 @@ and `clojure-mode` (this is very helpful when dealing with
`derived-mode-p` chec
- Navigation by sexp/lists might work differently on Emacs versions lower
than 31. Starting with version 31, Emacs uses Tree-sitter 'things' settings,
if
available, to rebind some commands.
-- The indentation of list elements with metadata is inconsistent with other
- collections. This inconsistency stems from the grammar's interpretation of
- nearly every definition or function call as a list. Therefore, modifying the
- indentation for list elements would adversely affect the indentation of
- numerous other forms.
## Frequently Asked Questions
diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el
index 2576132311..d828571775 100644
--- a/clojure-ts-mode.el
+++ b/clojure-ts-mode.el
@@ -266,6 +266,12 @@ values like this:
:safe #'listp
:type '(repeat string))
+(defcustom clojure-ts-completion-enabled t
+ "Enable built-in completion feature."
+ :package-version '(clojure-ts-mode . "0.5")
+ :safe #'booleanp
+ :type 'boolean)
+
(defvar clojure-ts-mode-remappings
'((clojure-mode . clojure-ts-mode)
(clojurescript-mode . clojure-ts-clojurescript-mode)
@@ -1561,26 +1567,28 @@ function literal."
"map_lit" "ns_map_lit" "vec_lit" "set_lit")
"A regular expression that matches nodes that can be treated as lists.")
+(defconst clojure-ts--defun-symbols-regex
+ (rx bol
+ (or "def"
+ "defn"
+ "defn-"
+ "definline"
+ "defrecord"
+ "defmacro"
+ "defmulti"
+ "defonce"
+ "defprotocol"
+ "deftest"
+ "deftest-"
+ "ns"
+ "definterface"
+ "deftype"
+ "defstruct")
+ eol))
+
(defun clojure-ts--defun-node-p (node)
"Return TRUE if NODE is a function or a var definition."
- (clojure-ts--list-node-sym-match-p node
- (rx bol
- (or "def"
- "defn"
- "defn-"
- "definline"
- "defrecord"
- "defmacro"
- "defmulti"
- "defonce"
- "defprotocol"
- "deftest"
- "deftest-"
- "ns"
- "definterface"
- "deftype"
- "defstruct")
- eol)))
+ (clojure-ts--list-node-sym-match-p node clojure-ts--defun-symbols-regex))
(defconst clojure-ts--markdown-inline-sexp-nodes
'("inline_link" "full_reference_link" "collapsed_reference_link"
@@ -2512,6 +2520,126 @@ before DELIM-OPEN."
map)
"Keymap for `clojure-ts-mode'.")
+;;; Completion
+
+(defconst clojure-ts--completion-query-defuns
+ (treesit-query-compile 'clojure
+ `((source
+ (list_lit
+ ((sym_lit) @sym
+ (:match ,clojure-ts--defun-symbols-regex @sym))
+ :anchor [(comment) (meta_lit) (old_meta_lit)] :*
+ :anchor ((sym_lit) @defun-candidate)))))
+ "Query that matches top-level definitions.")
+
+(defconst clojure-ts--completion-defn-with-args-sym-regex
+ (rx bol
+ (or "defn"
+ "defn-"
+ "fn"
+ "fn*"
+ "defmacro"
+ "defmethod")
+ eol)
+ "Regexp that matches a symbol of definition with arguments vector.")
+
+(defconst clojure-ts--completion-let-like-sym-regex
+ (rx bol
+ (or "let"
+ "if-let"
+ "when-let"
+ "if-some"
+ "when-some"
+ "loop"
+ "with-open"
+ "dotimes"
+ "with-local-vars")
+ eol)
+ "Regexp that matches a symbol of let-like form.")
+
+(defconst clojure-ts--completion-locals-query
+ (treesit-query-compile 'clojure `((vec_lit (sym_lit) @local-candidate)
+ (map_lit (sym_lit) @local-candidate)))
+ "Query that matches a local binding symbol.
+
+Symbold must be a direct child of a vector or a map. This query covers
+bindings vector as well as destructuring syntax.")
+
+(defconst clojure-ts--completion-annotations
+ (list 'defun-candidate " Definition"
+ 'local-candidate " Local variable")
+ "Property list of completion candidate type and annotation string.")
+
+(defun clojure-ts--completion-annotation-function (candidate)
+ "Return annotation for a completion CANDIDATE."
+ (thread-last minibuffer-completion-table
+ (alist-get candidate)
+ (plist-get clojure-ts--completion-annotations)))
+
+(defun clojure-ts--completion-defun-with-args-node-p (node)
+ "Return non-nil if NODE is a function definition with arguments."
+ (when-let* ((sym-name (clojure-ts--list-node-sym-text node)))
+ (string-match-p clojure-ts--completion-defn-with-args-sym-regex sym-name)))
+
+(defun clojure-ts--completion-fn-args-nodes ()
+ "Return a list of captured nodes that represent function arguments.
+
+The function traverses the syntax tree upwards and returns nodes from
+all functions along the way."
+ (let ((parent-defun (clojure-ts--parent-until
#'clojure-ts--completion-defun-with-args-node-p))
+ (captured-nodes))
+ (while parent-defun
+ (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))))
+ captured-nodes))
+
+(defun clojure-ts--completion-let-like-node-p (node)
+ "Return non-nil if NODE is a let-like form."
+ (when-let* ((sym-name (clojure-ts--list-node-sym-text node)))
+ (string-match-p clojure-ts--completion-let-like-sym-regex sym-name)))
+
+(defun clojure-ts--completion-let-locals-nodes ()
+ "Return a list of captured nodes that represent bindings in let forms.
+
+The function tranverses the syntax tree upwards and returns nodes from
+all let bindings found along the way."
+ (let ((parent-let (clojure-ts--parent-until
#'clojure-ts--completion-let-like-node-p))
+ (captured-nodes))
+ (while parent-let
+ (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))))
+ captured-nodes))
+
+(defun clojure-ts-completion-at-point-function ()
+ "Return a completion table for the symbol around point."
+ (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)
+ (clojure-ts--completion-fn-args-nodes)
+ (clojure-ts--completion-let-locals-nodes))))
+ (list (car bounds)
+ (cdr bounds)
+ (thread-last nodes
+ ;; Remove node at point
+ (seq-remove (lambda (item) (= (treesit-node-end (cdr
item)) (point))))
+ ;; Remove unwanted captured nodes
+ (seq-filter (lambda (item)
+ (not (member (car item) '(sym kwd)))))
+ ;; Produce alist of candidates
+ (seq-map (lambda (item) (cons (treesit-node-text (cdr
item) t) (car item))))
+ ;; Remove duplicated candidates
+ (seq-uniq))
+ :exclusive 'no
+ :annotation-function #'clojure-ts--completion-annotation-function)))
+
(defvar clojure-ts-clojurescript-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map clojure-ts-mode-map)
@@ -2670,7 +2798,10 @@ REGEX-AVAILABLE."
clojure-ts--imenu-settings)
(when (boundp 'treesit-thing-settings) ;; Emacs 30+
- (setq-local treesit-thing-settings clojure-ts--thing-settings)))
+ (setq-local treesit-thing-settings clojure-ts--thing-settings))
+
+ (when clojure-ts-completion-enabled
+ (add-to-list 'completion-at-point-functions
#'clojure-ts-completion-at-point-function)))
;;;###autoload
(define-derived-mode clojure-ts-mode prog-mode "Clojure[TS]"
diff --git a/test/clojure-ts-mode-completion.el
b/test/clojure-ts-mode-completion.el
new file mode 100644
index 0000000000..1bc92cec10
--- /dev/null
+++ b/test/clojure-ts-mode-completion.el
@@ -0,0 +1,153 @@
+;;; clojure-ts-mode-completion.el --- clojure-ts-mode: completion tests -*-
lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Roman Rudakov
+
+;; Author: Roman Rudakov <[email protected]>
+;; Keywords:
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Completion is a unique `clojure-ts-mode' feature.
+
+;;; Code:
+
+(require 'clojure-ts-mode)
+(require 'buttercup)
+(require 'test-helper "test/test-helper")
+
+(describe "clojure-ts-complete-at-point-function"
+ ;; NOTE: This function returns unfiltered candidates, so prefix doesn't
really
+ ;; matter here.
+
+ (it "should complete global vars"
+ (with-clojure-ts-buffer-point "
+(def foo :first)
+
+(def bar :second)
+
+(defn baz
+ []
+ (println foo bar))
+
+b|"
+ (expect (nth 2 (clojure-ts-completion-at-point-function))
+ :to-equal '(("foo" . defun-candidate)
+ ("bar" . defun-candidate)
+ ("baz" . defun-candidate)))))
+
+ (it "should complete function arguments"
+ (with-clojure-ts-buffer-point "
+(def foo :first)
+
+(def bar :second)
+
+(defn baz
+ [username]
+ (println u|))"
+ (expect (nth 2 (clojure-ts-completion-at-point-function))
+ :to-equal '(("foo" . defun-candidate)
+ ("bar" . defun-candidate)
+ ("baz" . defun-candidate)
+ ("username" . local-candidate)))))
+
+ (it "should not complete function arguments outside of function"
+ (with-clojure-ts-buffer-point "
+(def foo :first)
+
+(def bar :second)
+
+(defn baz
+ [username]
+ (println bar))
+
+u|"
+ (expect (nth 2 (clojure-ts-completion-at-point-function))
+ :to-equal '(("foo" . defun-candidate)
+ ("bar" . defun-candidate)
+ ("baz" . defun-candidate)))))
+
+ (it "should complete destructured function arguments"
+ (with-clojure-ts-buffer-point "
+(defn baz
+ [{:keys [username]}]
+ (println u|))"
+ (expect (nth 2 (clojure-ts-completion-at-point-function))
+ :to-equal '(("baz" . defun-candidate)
+ ("username" . local-candidate))))
+
+ (with-clojure-ts-buffer-point "
+(defn baz
+ [{:strs [username]}]
+ (println u|))"
+ (expect (nth 2 (clojure-ts-completion-at-point-function))
+ :to-equal '(("baz" . defun-candidate)
+ ("username" . local-candidate))))
+
+ (with-clojure-ts-buffer-point "
+(defn baz
+ [{:syms [username]}]
+ (println u|))"
+ (expect (nth 2 (clojure-ts-completion-at-point-function))
+ :to-equal '(("baz" . defun-candidate)
+ ("username" . local-candidate))))
+
+ (with-clojure-ts-buffer-point "
+(defn baz
+ [{username :name}]
+ (println u|))"
+ (expect (nth 2 (clojure-ts-completion-at-point-function))
+ :to-equal '(("baz" . defun-candidate)
+ ("username" . local-candidate))))
+
+ (with-clojure-ts-buffer-point "
+(defn baz
+ [[first-name last-name]]
+ (println f|))"
+ (expect (nth 2 (clojure-ts-completion-at-point-function))
+ :to-equal '(("baz" . defun-candidate)
+ ("first-name" . local-candidate)
+ ("last-name" . local-candidate)))))
+
+ (it "should complete vector bindings"
+ (with-clojure-ts-buffer-point "
+(defn baz
+ [first-name]
+ (let [last-name \"Doe\"
+ address {:street \"Whatever\" :zip-code 2222}
+ {:keys [street zip-code]} address]
+ a|))"
+ (expect (nth 2 (clojure-ts-completion-at-point-function))
+ :to-equal '(("baz" . defun-candidate)
+ ("first-name" . local-candidate)
+ ("last-name" . local-candidate)
+ ("address" . local-candidate)
+ ("street" . local-candidate)
+ ("zip-code" . local-candidate)))))
+
+ (it "should not complete called function names"
+ (with-clojure-ts-buffer-point "
+(defn baz
+ [first-name]
+ (let [full-name (str first-name \"Doe\")]
+ s|))"
+ ;; `str' should not be among the candidates.
+ (expect (nth 2 (clojure-ts-completion-at-point-function))
+ :to-equal '(("baz" . defun-candidate)
+ ("first-name" . local-candidate)
+ ("full-name" . 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
new file mode 100644
index 0000000000..16b64de570
--- /dev/null
+++ b/test/samples/completion.clj
@@ -0,0 +1,56 @@
+(ns completion)
+
+(def my-var "Hello")
+(def my-another-var "World")
+
+(defn- my-function
+ "This is a docstring."
+ [some-arg]
+ (let [to-print (str "Hello" some-arg)]
+ (println my-var my-another-var to-print)))
+
+(fn [anon-arg]
+ anon-arg)
+
+(def hello-string "Hello")
+
+(defn complete-example
+ "Docstring won't interfere with completion."
+ [arg1 arg2 & {:keys [destructured]}]
+ ;; Here only function args and globals should be completed.
+ (println arg1 arg2 destructured)
+ (let [foo "bar" ; comment
+ baz ^String hello
+ map-var {:users/usename "Roma"}
+ {:users/keys [username]} map-var
+ another-map {:address "Universe"}
+ {custom-address :address} another-map
+ bar :kwd]
+ ;; Here let bindings are available in addition to globals and function
args.
+ (println arg1 foo map-var custom-address username)
+ (when-let [nested-var "Whatever"]
+ (with-open [output-stream (io/output-stream "some-file")]
+ (println foo
+ baz
+ hello
+ map-var
+ username
+ another-map
+ custom-address
+ bar)
+ ;; Here we should see everything
+ (output-stream nested-var output-stream another-map)))
+ ;; And here only let bindings, globals and function args again.
+ (println username)))
+
+(def vec-variable ["one" "two" "three"])
+
+(let [[one two three] vec-variable]
+ (println one two three))
+
+(defn nested-fn
+ [top-arg]
+ (filter (fn [item]
+ ;; Both arguments are available here.
+ (= item top-arg))
+ [1 2 3 4 5]))