Skip to content

Introduce clojure-ts-completion-at-point-function #108

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
169 changes: 150 additions & 19 deletions clojure-ts-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]"
Expand Down
153 changes: 153 additions & 0 deletions test/clojure-ts-mode-completion.el
Original file line number Diff line number Diff line change
@@ -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
Loading