Skip to content

Commit 95aed26

Browse files
rrudakovbbatsov
authored andcommitted
[#107] Introduce clojure-ts-completion-at-point-function
1 parent 39f93d4 commit 95aed26

File tree

5 files changed

+376
-25
lines changed

5 files changed

+376
-25
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
highlighting C++ syntax in Jank `native/raw` forms.
1616
- [#103](https://github.com/clojure-emacs/clojure-ts-mode/issues/103): Introduce `clojure-ts-clojurescript-use-js-parser` customization which
1717
allows highlighting JS syntax in ClojureScript `js*` forms.
18-
- Introduce the `clojure-ts-extra-def-forms` customization option to specify
18+
- [#104](https://github.com/clojure-emacs/clojure-ts-mode/pull/104): Introduce the `clojure-ts-extra-def-forms` customization option to specify
1919
additional `defn`-like forms that should be fontified.
20+
- Introduce completion feature and `clojure-ts-completion-enabled` customization.
2021

2122
## 0.4.0 (2025-05-15)
2223

README.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,21 @@ multi-arity function or macro. Function can be defined using `defn`, `fn` or
539539
By default prefix for all refactoring commands is `C-c C-r`. It can be changed
540540
by customizing `clojure-ts-refactor-map-prefix` variable.
541541

542+
## Code completion
543+
544+
`clojure-ts-mode` provides basic code completion functionality. Completion only
545+
works for the current source buffer and includes completion of top-level
546+
definitions and local bindings. This feature can be turned off by setting:
547+
548+
```emacs-lisp
549+
(setopt clojure-ts-completion-enabled nil)
550+
```
551+
552+
Here's the short video illustrating the feature with built-in completion (it
553+
should also work well with more advanced packages like company and corfu):
554+
555+
https://github.com/user-attachments/assets/7c37179f-5a5d-424f-9bd6-9c8525f6b2f7
556+
542557
## Migrating to clojure-ts-mode
543558

544559
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
576591
- Navigation by sexp/lists might work differently on Emacs versions lower
577592
than 31. Starting with version 31, Emacs uses Tree-sitter 'things' settings, if
578593
available, to rebind some commands.
579-
- The indentation of list elements with metadata is inconsistent with other
580-
collections. This inconsistency stems from the grammar's interpretation of
581-
nearly every definition or function call as a list. Therefore, modifying the
582-
indentation for list elements would adversely affect the indentation of
583-
numerous other forms.
584594

585595
## Frequently Asked Questions
586596

clojure-ts-mode.el

Lines changed: 150 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,12 @@ values like this:
266266
:safe #'listp
267267
:type '(repeat string))
268268

269+
(defcustom clojure-ts-completion-enabled t
270+
"Enable built-in completion feature."
271+
:package-version '(clojure-ts-mode . "0.5")
272+
:safe #'booleanp
273+
:type 'boolean)
274+
269275
(defvar clojure-ts-mode-remappings
270276
'((clojure-mode . clojure-ts-mode)
271277
(clojurescript-mode . clojure-ts-clojurescript-mode)
@@ -1561,26 +1567,28 @@ function literal."
15611567
"map_lit" "ns_map_lit" "vec_lit" "set_lit")
15621568
"A regular expression that matches nodes that can be treated as lists.")
15631569

1570+
(defconst clojure-ts--defun-symbols-regex
1571+
(rx bol
1572+
(or "def"
1573+
"defn"
1574+
"defn-"
1575+
"definline"
1576+
"defrecord"
1577+
"defmacro"
1578+
"defmulti"
1579+
"defonce"
1580+
"defprotocol"
1581+
"deftest"
1582+
"deftest-"
1583+
"ns"
1584+
"definterface"
1585+
"deftype"
1586+
"defstruct")
1587+
eol))
1588+
15641589
(defun clojure-ts--defun-node-p (node)
15651590
"Return TRUE if NODE is a function or a var definition."
1566-
(clojure-ts--list-node-sym-match-p node
1567-
(rx bol
1568-
(or "def"
1569-
"defn"
1570-
"defn-"
1571-
"definline"
1572-
"defrecord"
1573-
"defmacro"
1574-
"defmulti"
1575-
"defonce"
1576-
"defprotocol"
1577-
"deftest"
1578-
"deftest-"
1579-
"ns"
1580-
"definterface"
1581-
"deftype"
1582-
"defstruct")
1583-
eol)))
1591+
(clojure-ts--list-node-sym-match-p node clojure-ts--defun-symbols-regex))
15841592

15851593
(defconst clojure-ts--markdown-inline-sexp-nodes
15861594
'("inline_link" "full_reference_link" "collapsed_reference_link"
@@ -2512,6 +2520,126 @@ before DELIM-OPEN."
25122520
map)
25132521
"Keymap for `clojure-ts-mode'.")
25142522

2523+
;;; Completion
2524+
2525+
(defconst clojure-ts--completion-query-defuns
2526+
(treesit-query-compile 'clojure
2527+
`((source
2528+
(list_lit
2529+
((sym_lit) @sym
2530+
(:match ,clojure-ts--defun-symbols-regex @sym))
2531+
:anchor [(comment) (meta_lit) (old_meta_lit)] :*
2532+
:anchor ((sym_lit) @defun-candidate)))))
2533+
"Query that matches top-level definitions.")
2534+
2535+
(defconst clojure-ts--completion-defn-with-args-sym-regex
2536+
(rx bol
2537+
(or "defn"
2538+
"defn-"
2539+
"fn"
2540+
"fn*"
2541+
"defmacro"
2542+
"defmethod")
2543+
eol)
2544+
"Regexp that matches a symbol of definition with arguments vector.")
2545+
2546+
(defconst clojure-ts--completion-let-like-sym-regex
2547+
(rx bol
2548+
(or "let"
2549+
"if-let"
2550+
"when-let"
2551+
"if-some"
2552+
"when-some"
2553+
"loop"
2554+
"with-open"
2555+
"dotimes"
2556+
"with-local-vars")
2557+
eol)
2558+
"Regexp that matches a symbol of let-like form.")
2559+
2560+
(defconst clojure-ts--completion-locals-query
2561+
(treesit-query-compile 'clojure `((vec_lit (sym_lit) @local-candidate)
2562+
(map_lit (sym_lit) @local-candidate)))
2563+
"Query that matches a local binding symbol.
2564+
2565+
Symbold must be a direct child of a vector or a map. This query covers
2566+
bindings vector as well as destructuring syntax.")
2567+
2568+
(defconst clojure-ts--completion-annotations
2569+
(list 'defun-candidate " Definition"
2570+
'local-candidate " Local variable")
2571+
"Property list of completion candidate type and annotation string.")
2572+
2573+
(defun clojure-ts--completion-annotation-function (candidate)
2574+
"Return annotation for a completion CANDIDATE."
2575+
(thread-last minibuffer-completion-table
2576+
(alist-get candidate)
2577+
(plist-get clojure-ts--completion-annotations)))
2578+
2579+
(defun clojure-ts--completion-defun-with-args-node-p (node)
2580+
"Return non-nil if NODE is a function definition with arguments."
2581+
(when-let* ((sym-name (clojure-ts--list-node-sym-text node)))
2582+
(string-match-p clojure-ts--completion-defn-with-args-sym-regex sym-name)))
2583+
2584+
(defun clojure-ts--completion-fn-args-nodes ()
2585+
"Return a list of captured nodes that represent function arguments.
2586+
2587+
The function traverses the syntax tree upwards and returns nodes from
2588+
all functions along the way."
2589+
(let ((parent-defun (clojure-ts--parent-until #'clojure-ts--completion-defun-with-args-node-p))
2590+
(captured-nodes))
2591+
(while parent-defun
2592+
(when-let* ((args-vec (clojure-ts--node-child parent-defun "vec_lit")))
2593+
(setq captured-nodes
2594+
(append captured-nodes
2595+
(treesit-query-capture args-vec clojure-ts--completion-locals-query))
2596+
parent-defun (treesit-parent-until parent-defun
2597+
#'clojure-ts--completion-defun-with-args-node-p))))
2598+
captured-nodes))
2599+
2600+
(defun clojure-ts--completion-let-like-node-p (node)
2601+
"Return non-nil if NODE is a let-like form."
2602+
(when-let* ((sym-name (clojure-ts--list-node-sym-text node)))
2603+
(string-match-p clojure-ts--completion-let-like-sym-regex sym-name)))
2604+
2605+
(defun clojure-ts--completion-let-locals-nodes ()
2606+
"Return a list of captured nodes that represent bindings in let forms.
2607+
2608+
The function tranverses the syntax tree upwards and returns nodes from
2609+
all let bindings found along the way."
2610+
(let ((parent-let (clojure-ts--parent-until #'clojure-ts--completion-let-like-node-p))
2611+
(captured-nodes))
2612+
(while parent-let
2613+
(when-let* ((bindings-vec (clojure-ts--node-child parent-let "vec_lit")))
2614+
(setq captured-nodes
2615+
(append captured-nodes
2616+
(treesit-query-capture bindings-vec clojure-ts--completion-locals-query))
2617+
parent-let (treesit-parent-until parent-let
2618+
#'clojure-ts--completion-let-like-node-p))))
2619+
captured-nodes))
2620+
2621+
(defun clojure-ts-completion-at-point-function ()
2622+
"Return a completion table for the symbol around point."
2623+
(when-let* ((bounds (bounds-of-thing-at-point 'symbol))
2624+
(source (treesit-buffer-root-node 'clojure))
2625+
(nodes (append (treesit-query-capture source clojure-ts--completion-query-defuns)
2626+
(clojure-ts--completion-fn-args-nodes)
2627+
(clojure-ts--completion-let-locals-nodes))))
2628+
(list (car bounds)
2629+
(cdr bounds)
2630+
(thread-last nodes
2631+
;; Remove node at point
2632+
(seq-remove (lambda (item) (= (treesit-node-end (cdr item)) (point))))
2633+
;; Remove unwanted captured nodes
2634+
(seq-filter (lambda (item)
2635+
(not (member (car item) '(sym kwd)))))
2636+
;; Produce alist of candidates
2637+
(seq-map (lambda (item) (cons (treesit-node-text (cdr item) t) (car item))))
2638+
;; Remove duplicated candidates
2639+
(seq-uniq))
2640+
:exclusive 'no
2641+
:annotation-function #'clojure-ts--completion-annotation-function)))
2642+
25152643
(defvar clojure-ts-clojurescript-mode-map
25162644
(let ((map (make-sparse-keymap)))
25172645
(set-keymap-parent map clojure-ts-mode-map)
@@ -2670,7 +2798,10 @@ REGEX-AVAILABLE."
26702798
clojure-ts--imenu-settings)
26712799

26722800
(when (boundp 'treesit-thing-settings) ;; Emacs 30+
2673-
(setq-local treesit-thing-settings clojure-ts--thing-settings)))
2801+
(setq-local treesit-thing-settings clojure-ts--thing-settings))
2802+
2803+
(when clojure-ts-completion-enabled
2804+
(add-to-list 'completion-at-point-functions #'clojure-ts-completion-at-point-function)))
26742805

26752806
;;;###autoload
26762807
(define-derived-mode clojure-ts-mode prog-mode "Clojure[TS]"

test/clojure-ts-mode-completion.el

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
;;; clojure-ts-mode-completion.el --- clojure-ts-mode: completion tests -*- lexical-binding: t; -*-
2+
3+
;; Copyright (C) 2025 Roman Rudakov
4+
5+
;; Author: Roman Rudakov <[email protected]>
6+
;; Keywords:
7+
8+
;; This program is free software; you can redistribute it and/or modify
9+
;; it under the terms of the GNU General Public License as published by
10+
;; the Free Software Foundation, either version 3 of the License, or
11+
;; (at your option) any later version.
12+
13+
;; This program is distributed in the hope that it will be useful,
14+
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
;; GNU General Public License for more details.
17+
18+
;; You should have received a copy of the GNU General Public License
19+
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
20+
21+
;;; Commentary:
22+
23+
;; Completion is a unique `clojure-ts-mode' feature.
24+
25+
;;; Code:
26+
27+
(require 'clojure-ts-mode)
28+
(require 'buttercup)
29+
(require 'test-helper "test/test-helper")
30+
31+
(describe "clojure-ts-complete-at-point-function"
32+
;; NOTE: This function returns unfiltered candidates, so prefix doesn't really
33+
;; matter here.
34+
35+
(it "should complete global vars"
36+
(with-clojure-ts-buffer-point "
37+
(def foo :first)
38+
39+
(def bar :second)
40+
41+
(defn baz
42+
[]
43+
(println foo bar))
44+
45+
b|"
46+
(expect (nth 2 (clojure-ts-completion-at-point-function))
47+
:to-equal '(("foo" . defun-candidate)
48+
("bar" . defun-candidate)
49+
("baz" . defun-candidate)))))
50+
51+
(it "should complete function arguments"
52+
(with-clojure-ts-buffer-point "
53+
(def foo :first)
54+
55+
(def bar :second)
56+
57+
(defn baz
58+
[username]
59+
(println u|))"
60+
(expect (nth 2 (clojure-ts-completion-at-point-function))
61+
:to-equal '(("foo" . defun-candidate)
62+
("bar" . defun-candidate)
63+
("baz" . defun-candidate)
64+
("username" . local-candidate)))))
65+
66+
(it "should not complete function arguments outside of function"
67+
(with-clojure-ts-buffer-point "
68+
(def foo :first)
69+
70+
(def bar :second)
71+
72+
(defn baz
73+
[username]
74+
(println bar))
75+
76+
u|"
77+
(expect (nth 2 (clojure-ts-completion-at-point-function))
78+
:to-equal '(("foo" . defun-candidate)
79+
("bar" . defun-candidate)
80+
("baz" . defun-candidate)))))
81+
82+
(it "should complete destructured function arguments"
83+
(with-clojure-ts-buffer-point "
84+
(defn baz
85+
[{:keys [username]}]
86+
(println u|))"
87+
(expect (nth 2 (clojure-ts-completion-at-point-function))
88+
:to-equal '(("baz" . defun-candidate)
89+
("username" . local-candidate))))
90+
91+
(with-clojure-ts-buffer-point "
92+
(defn baz
93+
[{:strs [username]}]
94+
(println u|))"
95+
(expect (nth 2 (clojure-ts-completion-at-point-function))
96+
:to-equal '(("baz" . defun-candidate)
97+
("username" . local-candidate))))
98+
99+
(with-clojure-ts-buffer-point "
100+
(defn baz
101+
[{:syms [username]}]
102+
(println u|))"
103+
(expect (nth 2 (clojure-ts-completion-at-point-function))
104+
:to-equal '(("baz" . defun-candidate)
105+
("username" . local-candidate))))
106+
107+
(with-clojure-ts-buffer-point "
108+
(defn baz
109+
[{username :name}]
110+
(println u|))"
111+
(expect (nth 2 (clojure-ts-completion-at-point-function))
112+
:to-equal '(("baz" . defun-candidate)
113+
("username" . local-candidate))))
114+
115+
(with-clojure-ts-buffer-point "
116+
(defn baz
117+
[[first-name last-name]]
118+
(println f|))"
119+
(expect (nth 2 (clojure-ts-completion-at-point-function))
120+
:to-equal '(("baz" . defun-candidate)
121+
("first-name" . local-candidate)
122+
("last-name" . local-candidate)))))
123+
124+
(it "should complete vector bindings"
125+
(with-clojure-ts-buffer-point "
126+
(defn baz
127+
[first-name]
128+
(let [last-name \"Doe\"
129+
address {:street \"Whatever\" :zip-code 2222}
130+
{:keys [street zip-code]} address]
131+
a|))"
132+
(expect (nth 2 (clojure-ts-completion-at-point-function))
133+
:to-equal '(("baz" . defun-candidate)
134+
("first-name" . local-candidate)
135+
("last-name" . local-candidate)
136+
("address" . local-candidate)
137+
("street" . local-candidate)
138+
("zip-code" . local-candidate)))))
139+
140+
(it "should not complete called function names"
141+
(with-clojure-ts-buffer-point "
142+
(defn baz
143+
[first-name]
144+
(let [full-name (str first-name \"Doe\")]
145+
s|))"
146+
;; `str' should not be among the candidates.
147+
(expect (nth 2 (clojure-ts-completion-at-point-function))
148+
:to-equal '(("baz" . defun-candidate)
149+
("first-name" . local-candidate)
150+
("full-name" . local-candidate))))))
151+
152+
(provide 'clojure-ts-mode-completion)
153+
;;; clojure-ts-mode-completion.el ends here

0 commit comments

Comments
 (0)