|
58 | 58 | (require 'treesit)
|
59 | 59 | (require 'align)
|
60 | 60 | (require 'subr-x)
|
| 61 | +(require 'project) |
61 | 62 |
|
62 | 63 | (declare-function treesit-parser-create "treesit.c")
|
63 | 64 | (declare-function treesit-node-eq "treesit.c")
|
@@ -266,6 +267,52 @@ values like this:
|
266 | 267 | :safe #'booleanp
|
267 | 268 | :type 'boolean)
|
268 | 269 |
|
| 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 | + |
269 | 316 | (defvar clojure-ts-mode-remappings
|
270 | 317 | '((clojure-mode . clojure-ts-mode)
|
271 | 318 | (clojurescript-mode . clojure-ts-clojurescript-mode)
|
@@ -2689,6 +2736,146 @@ The command will prompt you to select one of the available sections."
|
2689 | 2736 | map)
|
2690 | 2737 | "Keymap for `clojure-ts-mode'.")
|
2691 | 2738 |
|
| 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 | + |
2692 | 2879 | ;;; Completion
|
2693 | 2880 |
|
2694 | 2881 | (defconst clojure-ts--completion-query-defuns
|
@@ -2978,6 +3165,8 @@ REGEX-AVAILABLE."
|
2978 | 3165 | outline-search-function #'treesit-outline-search
|
2979 | 3166 | outline-level #'clojure-ts--outline-level))
|
2980 | 3167 |
|
| 3168 | + (setq-local clojure-ts-expected-ns-function #'clojure-ts-expected-ns) |
| 3169 | + |
2981 | 3170 | (setq-local treesit-font-lock-settings
|
2982 | 3171 | (clojure-ts--font-lock-settings markdown-available regex-available))
|
2983 | 3172 | (setq-local treesit-font-lock-feature-list
|
@@ -3169,28 +3358,6 @@ Useful if you want to switch to the `clojure-mode's mode mappings."
|
3169 | 3358 | (add-to-list 'interpreter-mode-alist '("nbb" . clojure-ts-clojurescript-mode))))
|
3170 | 3359 | (message "Clojure TS Mode will not be activated as Tree-sitter support is missing."))
|
3171 | 3360 |
|
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 |
| - |
3194 | 3361 | (provide 'clojure-ts-mode)
|
3195 | 3362 |
|
3196 | 3363 | ;;; clojure-ts-mode.el ends here
|
0 commit comments