GitHub - clojure-emacs/clojure-ts-mode: The next generation Clojure major mode for Emacs, powered by TreeSitter (original) (raw)
Clojure Tree-sitter Mode
clojure-ts-mode is an Emacs major mode that provides font-lock (syntax highlighting), indentation, and navigation support for theClojure(Script) programming language, powered by thetree-sitter-clojure Tree-sitter grammar.
Rationale
clojure-mode has served us well for a very long time, but it suffers from a few long-standing problems, related to Emacs limitations baked into its design. The introduction of built-in support for Tree-sitter in Emacs 29 presents a natural opportunity to address many of them. Enter clojure-ts-mode, which makes use of Tree-sitter to provide:
- fast, accurate and more granular font-locking
- fast indentation
- common Emacs functionality like structured navigation,
imenu(an outline of a source buffer), current form inference (used internally by various Emacs modes and utilities), etc
Working with Tree-sitter is significantly easier than the legacy Emacs APIs for font-locking and indentation, which makes it easier to contribute to clojure-ts-mode, and to improve it in general.
Keep in mind that the transition to clojure-ts-mode won't happen overnight for several reasons:
- getting to feature parity with
clojure-modewill take some time - tools that depend on
clojure-modewill need to be updated to work withclojure-ts-mode - we still need to support users of older Emacs versions that don't support Tree-sitter
That's why clojure-ts-mode is being developed independently of clojure-modeand will one day replace it when the time is right. (e.g. 3 major Emacs version down the road, so circa Emacs 32)
You can read more about the vision for clojure-ts-mode here.
Current Status
Warning
This library is still under active development. Breaking changes should be expected.
The currently provided functionality should cover the needs of most Clojure programmers, but you can expect to encounter some bugs and missing functionality here and there.
Those will be addressed over the time, as more and more people use clojure-ts-mode.
Installation
Requirements
For clojure-ts-mode to work, you need Emacs 30+ built with Tree-sitter support. To check if your Emacs supports Tree-sitter run the following (e.g. by using M-:):
Additionally, you'll need to have Git and some C compiler (cc) installed and available in your $PATH (or Emacs's exec-path), for clojure-ts-mode to be able to install the required Tree-sitter grammars automatically.
Tip
As the Tree-sitter support in Emacs is still fairly new and under active development itself, for optimal results you should use the latest stable Emacs release or even the development version of Emacs. See the "Caveats" section for more on the subject.
Install clojure-ts-mode
Note
That's the recommended way to install clojure-ts-mode.
If you have git and a C compiler (cc) available on your system's PATH,clojure-ts-mode will install the grammars
clojure-ts-mode is available on MElPA andNonGNU ELPA. It can be installed with:
(package-install 'clojure-ts-mode)
package-vc
Emacs also includes package-vc-install, so you can run:
(package-vc-install "https://github.com/clojure-emacs/clojure-ts-mode")
to install this package from source.
Manual installation
You can install it by cloning the repository and adding it to your load path.
git clone https://github.com/clojure-emacs/clojure-ts-mode.git
(add-to-list 'load-path "~/path/to/clojure-ts-mode/")
Once installed, evaluate clojure-ts-mode.el and you should be ready to go.
Install Tree-sitter grammars
Note
clojure-ts-mode install the required grammars automatically, so for most people no manual actions will be required.
clojure-ts-mode makes use of the following Tree-sitter grammars:
- The experimental version Clojure grammar. This version includes a few improvements, which potentially will be promoted to a stable release (See the discussion). This grammar is required for proper work of
clojure-ts-mode. - markdown-inline, which will be used for docstrings if available and if
clojure-ts-use-markdown-inlineis enabled. - tree-sitter-regex, which will be used for regex literals if available and if
clojure-ts-use-regex-parseris notnil.
clojure-ts-clojurescript-mode can optionally use tree-sitter-javascript grammar to highlight JS syntax in js* forms. This is enabled by default and can be turned off by setting clojure-ts-clojurescript-use-js-parser to nil.
clojure-ts-jank-mode can optionally use tree-sitter-cpp grammar to highlight C++ syntax in native/raw forms. This is enabled by default and can be turned off by setting clojure-ts-jank-use-cpp-parser to nil.
If you have git and a C compiler (cc) available on your system's PATH,clojure-ts-mode will install the grammars when you first open a Clojure file and clojure-ts-ensure-grammars is set to t (the default). macOS users can install the required tools like this:
Similarly, Debian/Ubuntu users can do something like:
sudo apt install build-essential
This installs GCC, G++, make, and other essential development tools.
If clojure-ts-mode fails to automatically install the grammar, you have the option to install it manually. Please, refer to the installation instructions of each required grammar and make sure you're install the versions expected (seeclojure-ts-grammar-recipes for details).
If clojure-ts-ensure-grammars is enabled, clojure-ts-mode will try to upgrade the Clojure grammar if it's outdated. This might happen, when you activateclojure-ts-mode for the first time after package update. If grammar was previously installed, you might need to restart Emacs, because it has to reload the grammar binary.
Upgrading Tree-sitter grammars
To reinstall or upgrade Tree-sitter grammars, you can execute:
M-x clojure-ts-reinstall-grammars
This will install the latest compatible grammars, even if they are already installed.
Configuration
To see a list of available configuration options do M-x customize-group <RET> clojure-ts.
Most configuration changes will require reverting any active clojure-ts-mode buffers.
Remapping of clojure-mode buffers
By default, clojure-ts-mode assumes command over all buffers and file extensions previously associated with clojure-mode (and derived major modes like clojurescript-mode). To disable this remapping, set
(setopt clojure-ts-auto-remap nil)
You can also use the commands clojure-ts-activate / clojure-ts-deactivate to interactively change this behavior.
Indentation
clojure-ts-mode currently supports 2 different indentation strategies:
semantic, the default, which tries to match the indentation ofclojure-modeandcljfmtfixed, a simple indentation strategy outlined by Tonsky in a blog post
Set the var clojure-ts-indent-style to change it.
(setopt clojure-ts-indent-style 'fixed)
Tip
You can find this article comparing semantic and fixed indentation useful.
Customizing semantic indentation
The indentation of special forms and macros with bodies is controlled viaclojure-ts-semantic-indent-rules. Nearly all special forms and built-in macros with bodies have special indentation settings in clojure-ts-mode, which are aligned with cljfmt indent rules. You can add/alter the indentation settings in your personal config. Let's assume you want to indent ->> and -> like this:
(->> something ala bala portokala)
You can do so by putting the following in your config:
(setopt clojure-ts-semantic-indent-rules '(("->" . ((:block 1))) ("->>" . ((:block 1)))))
This means that the body of the ->/->> is after the first argument.
The default set of rules is defined asclojure-ts--semantic-indent-rules-defaults, any rule can be overridden using customization option.
Two types of rules are supported: :block and :inner, mirroring those in cljfmt. When a rule is defined as :block n, n represents the number of arguments preceding the body. When a rule is defined as :inner n, each form within the expression's body, nested n levels deep, is indented by two spaces. These rule definitions fully reflect the cljfmt rules.
For example:
dohas a rule((:block 0)).whenhas a rule((:block 1)).defnandfnhave a rule((:inner 0)).letfnhas a rule((:block 1) (:inner 2 0)).
Note that clojure-ts-semantic-indent-rules should be set using the customization interface or setopt; otherwise, it will not be applied correctly.
Project-specific indentation
Custom indentation rules can be set for individual projects. To achieve this, you need to create a .dir-locals.el file in the project root. The content should look like:
((clojure-ts-mode . ((clojure-ts-semantic-indent-rules . (("with-transaction" . ((:block 1))) ("with-retry" . ((:block 1))))))))
In order to apply directory-local variables to existing buffers, they must be "reverted" (reloaded).
Vertical alignment
You can vertically align sexps with C-c SPC. For instance, typing this combo on the following form:
(def my-map {:a-key 1 :other-key 2})
Leads to the following:
(def my-map {:a-key 1 :other-key 2})
This can also be done automatically (as part of indentation) by turning onclojure-ts-align-forms-automatically. This way it will happen whenever you select some code and hit TAB.
Forms that can be aligned vertically are configured via the following variables:
clojure-ts-align-reader-conditionals- align reader conditionals as if they were maps.clojure-ts-align-binding-forms- a customizable list of forms with let-like bindings that can be aligned vertically.clojure-ts-align-cond-forms- a customizable list of forms whose body elements can be aligned vertically. These forms respect the block semantic indentation rule (if configured) and align only the body forms, skipping N special arguments.clojure-ts-align-separator- determines whether blank lines prevent vertical alignment.
Font Locking
To highlight entire rich comment expression with the comment font face, set
(setopt clojure-ts-comment-macro-font-lock-body t)
By default this is nil, so that anything within a comment expression is highlighted like regular Clojure code.
Tip
You can customize the exact level of font-locking via the variablestreesit-font-lock-level (the default value is 3) andtreesit-font-lock-features-list. Check this sectionof the Emacs manual for more details.
Extending font-lock rules
In clojure-ts-mode it is possible to specify additional defn-like forms that should be fontified. For example to highlight the following form from Hiccup library as a function definition:
(defelem file-upload "Creates a file upload input." [name] (input-field "file" name nil))
You can add defelem to clojure-ts-extra-def-forms list like this:
(add-to-list 'clojure-ts-extra-def-forms "defelem")
or set this variable using setopt:
(setopt clojure-ts-extra-def-forms '("defelem"))
This setting will highlight defelem symbol, function name and the docstring.
Important
Setting clojure-ts-extra-def-forms won't change the indentation rule for these forms. For indentation rules you should useclojure-ts-semantic-indent-rules variable (see semantic indentation section).
Highlight markdown syntax in docstrings
By default Markdown syntax is highlighted in the docstrings usingmarkdown-inline grammar. To disable this feature use:
(setopt clojure-ts-use-markdown-inline nil)
Example of Markdown syntax highlighting:
Highlight regular expression syntax
By default syntax inside regex literals is highlighted usingregex grammar. To disable this feature use:
(setopt clojure-ts-use-regex-parser nil)
Example of regex syntax highlighting:
Navigation and Evaluation
To make forms inside of (comment ...) forms appear as top-level forms for evaluation and navigation, set
(setopt clojure-ts-toplevel-inside-comment-form t)
Fill paragraph
To change the maximal line length used by M-x prog-fill-reindent-defun (also bound to M-q by default) to reformat docstrings and comments it's possible to customize clojure-ts-fill-paragraph variable (by default set to the value of Emacs' fill-paragraph value).
Every new line in the docstrings is indented byclojure-ts-docstring-fill-prefix-width number of spaces (set to 2 by default which matches the clojure-mode settings).
imenu
clojure-ts-mode supports various types of definition that can be navigated using imenu, such as:
- namespace
- function
- macro
- var
- interface (forms such as
defprotocol,definterfaceanddefmulti) - class (forms such as
deftype,defrecordanddefstruct) - keyword (for example, spec definitions)
Integration with outline-minor-mode
clojure-ts-mode supports two integration variants withoutline-minor-mode. The default variant uses special top-level comments (level 1 heading starts with three semicolons, level 2 heading starts with four, etc.). The other variant treats def-like forms (the same forms produced by theimenu command) as outline headings. To use the second option, use the following customization:
(setopt clojure-ts-outline-variant 'imenu)
Refactoring support
Threading macros related features
There are a bunch of commands for threading and unwinding threaded Clojure forms:
clojure-ts-thread: Thread another form into the surrounding thread. Both->>/some->>and->/some->variants are supported.clojure-ts-unwind: Unwind a threaded expression. Supports both->>/some->>and->/some->.clojure-ts-thread-first-all: Introduce the thread first macro (->) and rewrite the entire form. With a prefix argument do not thread the last form.clojure-ts-thread-last-all: Introduce the thread last macro and rewrite the entire form. With a prefix argument do not thread the last form.clojure-ts-unwind-all: Fully unwind a threaded expression removing the threading macro.
Customize threading refactoring behavior
By default clojure-ts-thread-first-all and clojure-ts-thread-last-all will thread all nested expressions. For example this expression:
(->map (assoc {} :key "value") :lock)
After executing clojure-ts-thread-last-all will be converted to:
(-> {} (assoc :key "value") (->map :lock))
This behavior can be changed by setting:
(setopt clojure-ts-thread-all-but-last t)
Then the last expression will not be threaded and the result will be:
(-> (assoc {} :key "value") (->map :lock))
Cycling things
clojure-ts-cycle-keyword-string: Convert the string at point to a keyword and vice versa.clojure-ts-cycle-privacy: Cycle privacy ofdefs ordefns. Use metadata explicitly with settingclojure-ts-use-metadata-for-defn-privacytotfordefns too.clojure-ts-cycle-conditional: Change a surrounding conditional form to its negated counterpart, or vice versa (supportsif/if-notandwhen/when-not). Forif/if-notalso transposes the else and then branches, keeping the semantics the same as before.clojure-ts-cycle-not: Add or remove anotform around the current form.
Convert collection
Convert any given collection at point to list, quoted list, map, vector or set. The following commands are available:
clojure-ts-convert-collection-to-listclojure-ts-convert-collection-to-quoted-listclojure-ts-convert-collection-to-mapclojure-ts-convert-collection-to-vectorclojure-ts-convert-collection-to-set
Add arity to a function or macro
clojure-ts-add-arity: Add a new arity to an existing single-arity or multi-arity function or macro. Function can be defined using defn, fn ordefmethod form. This command also supports functions defined inside forms likeletfn, defprotol, reify, extend-protocol or proxy.
Default keybindings
| Keybinding | Command |
|---|---|
| C-: | clojure-ts-cycle-keyword-string |
| C-c SPC | clojure-ts-align |
| C-c C-r t / C-c C-r C-t | clojure-ts-thread |
| C-c C-r u / C-c C-r C-u | clojure-ts-unwind |
| C-c C-r f / C-c C-r C-f | clojure-ts-thread-first-all |
| C-c C-r l / C-c C-r C-l | clojure-ts-thread-last-all |
| C-c C-r p / C-c C-r C-p | clojure-ts-cycle-privacy |
| C-c C-r ( / C-c C-r C-( | clojure-ts-convert-collection-to-list |
| C-c C-r ' / C-c C-r C-' | clojure-ts-convert-collection-to-quoted-list |
| C-c C-r { / C-c C-r C-{ | clojure-ts-convert-collection-to-map |
| C-c C-r [ / C-c C-r C-[ | clojure-ts-convert-collection-to-vector |
| C-c C-r # / C-c C-r C-# | clojure-ts-convert-collection-to-set |
| C-c C-r c / C-c C-r C-c | clojure-ts-cycle-conditional |
| C-c C-r o / C-c C-r C-o | clojure-ts-cycle-not |
| C-c C-r a / C-c C-r C-a | clojure-ts-add-arity |
Customize refactoring commands prefix
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:
(setopt clojure-ts-completion-enabled nil)
Here's the short video illustrating the feature with Emacs's built-in completion UI (it should also work well with more advanced packages like company and corfu):
completion.mp4
Migrating to clojure-ts-mode
If you are migrating to clojure-ts-mode note that clojure-mode is still required for CIDER and clj-refactor packages to work properly.
After installing the package do the following:
- Check the value of
clojure-mode-hookand copy all relevant hooks toclojure-ts-mode-hook.
(add-hook 'clojure-ts-mode-hook #'cider-mode) (add-hook 'clojure-ts-mode-hook #'enable-paredit-mode) (add-hook 'clojure-ts-mode-hook #'rainbow-delimiters-mode) (add-hook 'clojure-ts-mode-hook #'clj-refactor-mode)
- Update
.dir-locals.elin all of your Clojure projects to activate directory local variables inclojure-ts-mode.
((clojure-mode (cider-clojure-cli-aliases . ":test:repl")) (clojure-ts-mode (cider-clojure-cli-aliases . ":test:repl")))
Caveats
As the Tree-sitter Emacs APIs are new and keep evolving there are some differences in the behavior of clojure-ts-mode on different Emacs versions. Here are some notable examples:
- On Emacs 29 the parent mode is
prog-mode, but on Emacs 30+ it's bothprog-modeandclojure-mode(this is very helpful when dealing withderived-mode-pchecks) - 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.
- If you set
clojure-ts-extra-def-forms,clojure-ts-modewill highlight the specified forms, including their docstrings, in a manner similar to Clojure'sdefn. However, Markdown syntax will not be highlighted within these custom docstrings.
Frequently Asked Questions
What clojure-mode features are currently missing?
As of version 0.5.x, clojure-ts-mode provides almost all clojure-mode features. Currently only a few refactoring commands are missing.
Does clojure-ts-mode work with CIDER?
Yes! Preliminary support for clojure-ts-mode was released in CIDER 1.14. Note thatclojure-mode is still needed for some APIs that haven't yet been ported toclojure-ts-mode.
For now, when you take care of the keybindings for the CIDER commands you use and ensure cider-mode is enabled for clojure-ts-mode buffers in your config, most functionality should already work:
(add-hook 'clojure-ts-mode-hook #'cider-mode)
Check out this article for more details.
Note
The dynamic indentation feature in CIDER requires clojure-ts-mode 0.3+.
Does clojure-ts-mode work with inf-clojure?
Yes, it does. inf-clojure 3.3+ supports clojure-ts-mode.
Why does clojure-ts-mode require Emacs 30?
You might be wondering why does clojure-ts-mode require Emacs 30 instead of Emacs 29, which introduced the built-in Tree-sitter support. The answer is simple - the initial Tree-sitter support in Emacs 29 had quite a few issues and we felt it's better to nudge most people interested in using it to Emacs 30, which fixed a lot of the problems.
Contributing
We welcome contributions of any kind!
If you're not familiar with Tree-sitter, a good place to start is ourdesign documentation, which explains how Tree-sitter works in Emacs in broad strokes and covers some of the design decisions we've made a long the way.
We're using Eldev as our build tool, so you'll have to install it. We also provide a simple Makefile with targets invoking Eldev. You only need to know a couple of them:
The process of releasing a new version of clojure-ts-mode is documented here.
License
Copyright © 2022-2025 Danny Freeman, Bozhidar Batsov and contributors.
Distributed under the GNU General Public License; type C-h C-c to view it.

