Skip to content

Commit ee94f1e

Browse files
rrudakovbbatsov
authored andcommitted
[#117] Add some ns manipulation functions from clojure-mode
- Add project helper functions. - Add a few defcustom's - Add ns helper functions. NOTE: clojure-ts-sort-ns is not implemented, I think we should try to leverage Tree-sitter for that somehow, so I need more time to think about the implementation.
1 parent 603660f commit ee94f1e

File tree

7 files changed

+243
-23
lines changed

7 files changed

+243
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- [#116](https://github.com/clojure-emacs/clojure-ts-mode/pull/116): Extend built-in completion to complete all imported symbols from an `ns`
1010
form.
1111
- Add documentation and bug reporting commands from `clojure-mode`.
12+
- Add some ns manipulation functions from `clojure-mode`.
1213

1314
## 0.5.1 (2025-06-17)
1415

clojure-ts-mode.el

Lines changed: 189 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
(require 'treesit)
5959
(require 'align)
6060
(require 'subr-x)
61+
(require 'project)
6162

6263
(declare-function treesit-parser-create "treesit.c")
6364
(declare-function treesit-node-eq "treesit.c")
@@ -266,6 +267,52 @@ values like this:
266267
:safe #'booleanp
267268
:type 'boolean)
268269

270+
(defcustom clojure-ts-build-tool-files
271+
'("project.clj" ; Leiningen
272+
"build.boot" ; Boot
273+
"build.gradle" ; Gradle
274+
"build.gradle.kts" ; Gradle
275+
"deps.edn" ; Clojure CLI (a.k.a. tools.deps)
276+
"shadow-cljs.edn" ; shadow-cljs
277+
"bb.edn" ; babashka
278+
"nbb.edn" ; nbb
279+
"basilisp.edn" ; Basilisp (Python)
280+
)
281+
"A list of files, which identify a Clojure project's root."
282+
:type '(repeat string)
283+
:package-version '(clojure-ts-mode . "0.6.0")
284+
:safe (lambda (value)
285+
(and (listp value)
286+
(cl-every 'stringp value))))
287+
288+
(defcustom clojure-ts-cache-project-dir t
289+
"Whether to cache the results of `clojure-ts-project-dir'."
290+
:type 'boolean
291+
:safe #'booleanp
292+
:package-version '(clojure-ts-mode . "0.6.0"))
293+
294+
(defcustom clojure-ts-cache-ns nil
295+
"Whether to cache the results of `clojure-ts-find-ns'.
296+
297+
Note that this won't work well in buffers with multiple namespace
298+
declarations (which rarely occur in practice) and you'll have to
299+
invalidate this manually after changing the ns for a buffer. If you
300+
update the ns using `clojure-ts-update-ns' the cached value will be
301+
updated automatically."
302+
:type 'boolean
303+
:safe #'booleanp
304+
:package-version '(clojure-ts-mode . "0.6.0"))
305+
306+
(defcustom clojure-ts-directory-prefixes
307+
'("^\\(?:[^/]+/\\)*clj[csxd]*/")
308+
"A list of directory prefixes used by `clojure-expected-ns'.
309+
The prefixes are used to generate the correct namespace."
310+
:type '(repeat string)
311+
:package-version '(clojure-mode . "0.6.0")
312+
:safe (lambda (value)
313+
(and (listp value)
314+
(cl-every 'stringp value))))
315+
269316
(defvar clojure-ts-mode-remappings
270317
'((clojure-mode . clojure-ts-mode)
271318
(clojurescript-mode . clojure-ts-clojurescript-mode)
@@ -2689,6 +2736,146 @@ The command will prompt you to select one of the available sections."
26892736
map)
26902737
"Keymap for `clojure-ts-mode'.")
26912738

2739+
;;; Project helpers
2740+
2741+
(defun clojure-ts-project-root-path (&optional dir-name)
2742+
"Return the absolute path to the project's root directory.
2743+
2744+
Uses `default-directory' if DIR-NAME is nil. Return nil if not inside
2745+
of a project.
2746+
2747+
NOTE: this function uses `project.el' internally, so if Clojure source
2748+
is located in a non-Clojure project, but still under version control,
2749+
the root of the project will be returned."
2750+
(let ((default-directory (or dir-name default-directory))
2751+
(project-vc-extra-root-markers clojure-ts-build-tool-files))
2752+
(expand-file-name (project-root (project-current)))))
2753+
2754+
(defcustom clojure-ts-project-root-function #'clojure-ts-project-root-path
2755+
"Function to locate Clojure project root directory."
2756+
:type 'function
2757+
:risky t
2758+
:package-version '(clojure-ts-mode . "0.6.0"))
2759+
2760+
(defvar-local clojure-ts-cached-project-dir nil
2761+
"A project dir cache used to speed up related operations.")
2762+
2763+
(defun clojure-ts-project-dir (&optional dir-name)
2764+
"Return an absolute path to the project's root directory.
2765+
2766+
Call is delegated down to `clojure-ts-project-root-function' with
2767+
optional DIR-NAME as argument.
2768+
2769+
When `clojure-ts-cache-project-dir' is non-nil, the result of the
2770+
command is cached in a buffer local variable
2771+
`clojure-ts-cached-project-dir'."
2772+
(let ((project-dir (or clojure-ts-cached-project-dir
2773+
(funcall clojure-ts-project-root-function dir-name))))
2774+
(when (and clojure-ts-cache-project-dir
2775+
(derived-mode-p 'clojure-ts-mode)
2776+
(not clojure-ts-cached-project-dir))
2777+
(setq-local clojure-ts-cached-project-dir project-dir))
2778+
project-dir))
2779+
2780+
(defun clojure-ts-project-relative-path (path)
2781+
"Denormalize PATH by making it relative to the project root."
2782+
(file-relative-name path (clojure-ts-project-dir)))
2783+
2784+
;;; ns manipulation
2785+
2786+
(defun clojure-ts-expected-ns (&optional path)
2787+
"Return the namespace matching PATH.
2788+
2789+
PATH is expected to be an absolute file path.
2790+
2791+
If PATH is nil, use the path to the file backing the current buffer."
2792+
(when-let* ((path (or path (when-let* ((buf-file-name (buffer-file-name)))
2793+
(file-truename buf-file-name))))
2794+
(relative (clojure-ts-project-relative-path path))
2795+
;; Drop prefix from ns for projects with structure
2796+
;; src/{clj,cljs,cljc}
2797+
(without-prefix (seq-reduce (lambda (acc regex)
2798+
(replace-regexp-in-string regex "" acc))
2799+
clojure-ts-directory-prefixes
2800+
relative)))
2801+
(thread-last without-prefix
2802+
(file-name-sans-extension)
2803+
(string-replace "_" "-")
2804+
(string-replace "/" "."))))
2805+
2806+
(defvar-local clojure-ts-expected-ns-function nil
2807+
"The function used to determine the expected namespace of a file.
2808+
2809+
`clojure-ts-mode' ships a basic function named `clojure-ts-expected-ns'
2810+
that does basic heuristics to figure this out. It can be redefined by
2811+
other packages to provide a more complex version.")
2812+
2813+
(defun clojure-ts-insert-ns-form-at-point ()
2814+
"Insert a namespace form at point."
2815+
(interactive)
2816+
(insert (format "(ns %s)" (funcall clojure-ts-expected-ns-function))))
2817+
2818+
(defun clojure-ts-insert-ns-form ()
2819+
"Insert a namespace form at the beginning of the buffer."
2820+
(interactive)
2821+
(widen)
2822+
(goto-char (point-min))
2823+
(clojure-ts-insert-ns-form-at-point))
2824+
2825+
(defvar-local clojure-ts-cached-ns nil
2826+
"A buffer ns cache to speed up ns-related operations.")
2827+
2828+
(defconst clojure-ts--find-ns-query
2829+
(treesit-query-compile
2830+
'clojure
2831+
'(((source (list_lit
2832+
:anchor [(comment) (meta_lit) (old_meta_lit)] :*
2833+
:anchor (sym_lit name: (sym_name) @ns)
2834+
:anchor [(comment) (meta_lit) (old_meta_lit)] :*
2835+
:anchor (sym_lit name: (sym_name) @ns-name)))
2836+
(:equal @ns "ns"))
2837+
((source (list_lit
2838+
:anchor [(comment) (meta_lit) (old_meta_lit)] :*
2839+
:anchor (sym_lit name: (sym_name) @in-ns)
2840+
:anchor [(comment) (meta_lit) (old_meta_lit)] :*
2841+
:anchor (quoting_lit
2842+
:anchor (sym_lit name: (sym_name) @ns-name))))
2843+
(:equal @in-ns "in-ns"))))
2844+
"Compiled Tree-sitter query to capture Clojure ns node.")
2845+
2846+
(defun clojure-ts-find-ns ()
2847+
"Return the name of the current namespace."
2848+
(if (and clojure-ts-cache-ns clojure-ts-cached-ns)
2849+
clojure-ts-cached-ns
2850+
(when-let* ((nodes (treesit-query-capture 'clojure clojure-ts--find-ns-query))
2851+
(ns-name-node (cdr (assoc 'ns-name nodes)))
2852+
(ns-name (treesit-node-text ns-name-node t)))
2853+
(when clojure-ts-cache-ns
2854+
(setq-local clojure-ts-cached-ns ns-name))
2855+
;; Set the match data, so the namespace could be easily replaced.
2856+
(let ((start (treesit-node-start ns-name-node))
2857+
(end (treesit-node-end ns-name-node)))
2858+
(set-match-data (list start end)))
2859+
ns-name)))
2860+
2861+
(defun clojure-ts-update-ns ()
2862+
"Update the namespace of the current buffer.
2863+
2864+
Useful if a file has been renamed."
2865+
(interactive)
2866+
(when-let* ((ns-name (funcall clojure-ts-expected-ns-function)))
2867+
(save-excursion
2868+
(save-match-data
2869+
(if (clojure-ts-find-ns)
2870+
(progn
2871+
;; This relies on the match data, set by `clojure-ts-find-ns'
2872+
;; function.
2873+
(replace-match ns-name nil nil nil 0)
2874+
(message "ns form updated to `%s'" ns-name)
2875+
(when clojure-ts-cache-ns
2876+
(setq-local clojure-ts-cached-ns ns-name)))
2877+
(user-error "Can't find ns form"))))))
2878+
26922879
;;; Completion
26932880

26942881
(defconst clojure-ts--completion-query-defuns
@@ -2978,6 +3165,8 @@ REGEX-AVAILABLE."
29783165
outline-search-function #'treesit-outline-search
29793166
outline-level #'clojure-ts--outline-level))
29803167

3168+
(setq-local clojure-ts-expected-ns-function #'clojure-ts-expected-ns)
3169+
29813170
(setq-local treesit-font-lock-settings
29823171
(clojure-ts--font-lock-settings markdown-available regex-available))
29833172
(setq-local treesit-font-lock-feature-list
@@ -3169,28 +3358,6 @@ Useful if you want to switch to the `clojure-mode's mode mappings."
31693358
(add-to-list 'interpreter-mode-alist '("nbb" . clojure-ts-clojurescript-mode))))
31703359
(message "Clojure TS Mode will not be activated as Tree-sitter support is missing."))
31713360

3172-
(defvar clojure-ts--find-ns-query
3173-
(treesit-query-compile
3174-
'clojure
3175-
'(((source (list_lit
3176-
:anchor [(comment) (meta_lit) (old_meta_lit)] :*
3177-
:anchor (sym_lit name: (sym_name) @ns)
3178-
:anchor [(comment) (meta_lit) (old_meta_lit)] :*
3179-
:anchor (sym_lit name: (sym_name) @ns-name)))
3180-
(:equal @ns "ns"))
3181-
((source (list_lit
3182-
:anchor [(comment) (meta_lit) (old_meta_lit)] :*
3183-
:anchor (sym_lit name: (sym_name) @in-ns)
3184-
:anchor [(comment) (meta_lit) (old_meta_lit)] :*
3185-
:anchor (quoting_lit
3186-
:anchor (sym_lit name: (sym_name) @ns-name))))
3187-
(:equal @in-ns "in-ns")))))
3188-
3189-
(defun clojure-ts-find-ns ()
3190-
"Return the name of the current namespace."
3191-
(let ((nodes (treesit-query-capture 'clojure clojure-ts--find-ns-query)))
3192-
(treesit-node-text (cdr (assoc 'ns-name nodes)) t)))
3193-
31943361
(provide 'clojure-ts-mode)
31953362

31963363
;;; clojure-ts-mode.el ends here

test/clojure-ts-mode-util-test.el

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,55 @@
2121

2222
;; The unit test suite of Clojure TS Mode
2323

24+
;;; Code:
25+
2426
(require 'clojure-ts-mode)
2527
(require 'buttercup)
28+
(require 'test-helper "test/test-helper")
2629

2730
(describe "clojure-ts-mode-version"
2831
(it "should not be nil"
2932
(expect clojure-ts-mode-version)))
3033

34+
(defvar clojure-ts-cache-project)
35+
36+
(let ((project-dir "/home/user/projects/my-project/")
37+
(clj-file-path "/home/user/projects/my-project/src/clj/my_project/my_ns/my_file.clj")
38+
(project-relative-clj-file-path "src/clj/my_project/my_ns/my_file.clj")
39+
(clj-file-ns "my-project.my-ns.my-file")
40+
(clojure-ts-cache-project nil))
41+
42+
(describe "clojure-ts-project-root-path"
43+
(it "nbb subdir"
44+
(with-temp-dir temp-dir
45+
(let* ((bb-edn (expand-file-name "nbb.edn" temp-dir))
46+
(bb-edn-src (expand-file-name "src" temp-dir)))
47+
(write-region "{}" nil bb-edn)
48+
(make-directory bb-edn-src)
49+
(expect (expand-file-name (clojure-ts-project-dir bb-edn-src))
50+
:to-equal (file-name-as-directory temp-dir))))))
51+
52+
(describe "clojure-ts-project-relative-path"
53+
(cl-letf (((symbol-function 'clojure-ts-project-dir) (lambda () project-dir)))
54+
(expect (clojure-ts-project-relative-path clj-file-path)
55+
:to-equal project-relative-clj-file-path)))
56+
57+
(describe "clojure-ts-expected-ns"
58+
(it "should return the namespace matching a path"
59+
(cl-letf (((symbol-function 'clojure-ts-project-relative-path)
60+
(lambda (&optional _current-buffer-file-name)
61+
project-relative-clj-file-path)))
62+
(expect (clojure-ts-expected-ns clj-file-path)
63+
:to-equal clj-file-ns)))
64+
65+
(it "should return the namespace even without a path"
66+
(cl-letf (((symbol-function 'clojure-ts-project-relative-path)
67+
(lambda (&optional _current-buffer-file-name)
68+
project-relative-clj-file-path)))
69+
(expect (let ((buffer-file-name clj-file-path))
70+
(clojure-ts-expected-ns))
71+
:to-equal clj-file-ns)))))
72+
3173
(describe "clojure-ts-find-ns"
3274
(it "should find common namespace declarations"
3375
(with-clojure-ts-buffer "(ns foo)"

test/samples/deps-project/deps.edn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{:paths ["src/clj"]}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
(ns hello-clj.world
2+
(:require
3+
;; This is a comment
4+
[clojure.string :as str]
5+
;; Hello world
6+
[clojure.math :as math])
7+
(:import
8+
(java.util.time Instant ZonedDateTime)))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
(ns hello.world)

test/samples/refactoring.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,4 +146,4 @@
146146
clojure.lang.IPersistentMap
147147
(set-parameter [])
148148
(set-parameter [m ^PreparedStatement s i]
149-
(.setObject| s i (->pgobject m))))
149+
(.setObject s i (->pgobject m))))

0 commit comments

Comments
 (0)