diff --git a/.github/workflows/github_release.yml b/.github/workflows/github_release.yml
new file mode 100644
index 0000000..390eacc
--- /dev/null
+++ b/.github/workflows/github_release.yml
@@ -0,0 +1,27 @@
+name: Create GitHub Release
+
+on:
+ push:
+ tags:
+ - "v*" # Trigger when a version tag is pushed (e.g., v1.0.0)
+
+jobs:
+ create-release:
+ runs-on: ubuntu-latest
+
+ permissions:
+ contents: write
+
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ - name: Create GitHub Release with Auto-Generated Notes
+ uses: ncipollo/release-action@v1
+ with:
+ tag: ${{ github.ref_name }}
+ name: clojure-ts-mode ${{ github.ref_name }}
+ prerelease: ${{ contains(github.ref, '-rc') || contains(github.ref, '-alpha') || contains(github.ref, '-beta') }}
+ generateReleaseNotes: true # Auto-generate release notes based on PRs and commits
+ # TODO: Use bodyFile to get the contents from changelog
+ token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3ba2af9..80a7f02 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,26 @@
## main (unreleased)
+## 0.4.0 (2025-05-15)
+
+- [#16](https://github.com/clojure-emacs/clojure-ts-mode/issues/16): Introduce `clojure-ts-align`.
+- [#11](https://github.com/clojure-emacs/clojure-ts-mode/issues/11): Enable regex syntax highlighting.
+- [#16](https://github.com/clojure-emacs/clojure-ts-mode/issues/16): Add support for automatic aligning forms.
+- [#82](https://github.com/clojure-emacs/clojure-ts-mode/issues/82): Introduce `clojure-ts-outline-variant`.
+- [#86](https://github.com/clojure-emacs/clojure-ts-mode/pull/86): Better handling of function literals:
+ - Syntax highlighting of built-in keywords.
+ - Consistent indentation with regular forms.
+ - Support for automatic aligning forms.
+- [#88](https://github.com/clojure-emacs/clojure-ts-mode/pull/88): Introduce `clojure-ts-unwind` and `clojure-ts-unwind-all`.
+- [#89](https://github.com/clojure-emacs/clojure-ts-mode/pull/89): Introduce `clojure-ts-thread`, `clojure-ts-thread-first-all` and
+ `clojure-ts-thread-last-all`.
+- [#90](https://github.com/clojure-emacs/clojure-ts-mode/pull/90): Introduce `clojure-ts-cycle-privacy`.
+- [#91](https://github.com/clojure-emacs/clojure-ts-mode/pull/91): Introduce `clojure-ts-cycle-keyword-string`.
+- [#92](https://github.com/clojure-emacs/clojure-ts-mode/pull/92): Add commands to convert between collections types.
+- [#93](https://github.com/clojure-emacs/clojure-ts-mode/pull/93): Introduce `clojure-ts-add-arity`.
+- [#94](https://github.com/clojure-emacs/clojure-ts-mode/pull/94): Add indentation rules and `clojure-ts-align` support for namespaced maps.
+- Introduce `clojure-ts-cycle-conditional` and `clojure-ts-cycle-not`.
+
## 0.3.0 (2025-04-15)
- [#62](https://github.com/clojure-emacs/clojure-ts-mode/issues/62): Define `list` "thing" to improve navigation in Emacs 31.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b72898e..9a6a3fe 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -35,21 +35,9 @@ clojure-ts-mode (version 2.1.1)
* Open a [pull request][4] that relates to *only* one subject with a clear title
and description in grammatically correct, complete sentences.
-## I don't have a github account
-
-or maybe you would rather use email. That is okay.
-
-If you prefer you can also send a message to the [mailing list][7].
-This mailing list is not the [primary issue tracker][1].
-All the same etiquette rules above apply to the mailing list as well.
-Submitted patches will be turned into pull requests.
-Any issues reported on the mailing list will be copied to the issue tracker
-where the primary work will take place.
-
[1]: https://github.com/clojure-emacs/clojure-ts-mode/issues
[2]: http://gun.io/blog/how-to-github-fork-branch-and-pull-request
[3]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
[4]: https://help.github.com/articles/using-pull-requests
[5]: http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html
[6]: https://github.com/clojure-emacs/clojure-ts-mode/blob/master/CHANGELOG.md
-[7]: https://lists.sr.ht/~dannyfreeman/clojure-ts-mode
diff --git a/README.md b/README.md
index d5407bc..d10ca0c 100644
--- a/README.md
+++ b/README.md
@@ -4,13 +4,13 @@
[![License GPL 3][badge-license]][copying]
[](https://github.com/clojure-emacs/clojure-ts-mode/actions/workflows/lint-emacs.yml)
-# Clojure Tree-Sitter Mode
+# Clojure Tree-sitter Mode
`clojure-ts-mode` is an Emacs major mode that provides font-lock (syntax
highlighting), indentation, and navigation support for the
[Clojure(Script) programming language](http://clojure.org), powered by the
[tree-sitter-clojure](https://github.com/sogaiu/tree-sitter-clojure)
-[tree-sitter](https://tree-sitter.github.io/tree-sitter/) grammar.
+[Tree-sitter](https://tree-sitter.github.io/tree-sitter/) grammar.
## Rationale
@@ -19,13 +19,15 @@ for a very long time, but it suffers from a few [long-standing
problems](https://github.com/clojure-emacs/clojure-mode#caveats), 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 TreeSitter to provide:
+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
+- 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 TreeSitter is significantly easier than the legacy Emacs APIs for font-locking and
+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:
@@ -55,8 +57,8 @@ Those will be addressed over the time, as more and more people use `clojure-ts-m
### Requirements
-For `clojure-ts-mode` to work, you need Emacs 30+ built with TreeSitter support.
-To check if your Emacs supports TreeSitter run the following (e.g. by using `M-:`):
+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-:`):
``` emacs-lisp
(treesit-available-p)
@@ -64,11 +66,11 @@ To check if your Emacs supports TreeSitter 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
-TreeSitter grammars automatically.
+Tree-sitter grammars automatically.
> [!TIP]
>
-> As the TreeSitter support in Emacs is still fairly new and under active development itself, for optimal
+> 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.
@@ -114,14 +116,14 @@ git clone https://github.com/clojure-emacs/clojure-ts-mode.git
Once installed, evaluate `clojure-ts-mode.el` and you should be ready to go.
-### Install tree-sitter grammars
+### 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 two TreeSitter grammars to work properly:
+`clojure-ts-mode` makes use of two Tree-sitter grammars to work properly:
- The Clojure grammar, mentioned earlier
- [markdown-inline](https://github.com/MDeiml/tree-sitter-markdown), which
@@ -137,9 +139,9 @@ option to install it manually, Please, refer to the installation instructions of
each required grammar and make sure you're install the versions expected. (see
`clojure-ts-grammar-recipes` for details)
-### Upgrading tree-sitter grammars
+### Upgrading Tree-sitter grammars
-To reinstall or upgrade TreeSitter grammars, you can execute:
+To reinstall or upgrade Tree-sitter grammars, you can execute:
```emacs-lisp
M-x clojure-ts-reinstall-grammars
@@ -156,13 +158,16 @@ Most configuration changes will require reverting any active `clojure-ts-mode` b
### 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
+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
``` emacs-lisp
(setopt clojure-ts-auto-remap nil)
```
-You can also use the commands `clojure-ts-activate` / `clojure-ts-deactivate` to interactively change this behavior.
+You can also use the commands `clojure-ts-activate` / `clojure-ts-deactivate` to
+interactively change this behavior.
### Indentation
@@ -216,6 +221,7 @@ within the expression's body, nested `n` levels deep, is indented by two
spaces. These rule definitions fully reflect the [cljfmt rules](https://github.com/weavejester/cljfmt/blob/0.13.0/docs/INDENTS.md).
For example:
+
- `do` has a rule `((:block 0))`.
- `when` has a rule `((:block 1))`.
- `defn` and `fn` have a rule `((:inner 0))`.
@@ -239,6 +245,42 @@ should look like:
In order to apply directory-local variables to existing buffers, they must be
reverted.
+### Vertical alignment
+
+You can vertically align sexps with `C-c SPC`. For instance, typing this combo
+on the following form:
+
+```clojure
+(def my-map
+ {:a-key 1
+ :other-key 2})
+```
+
+Leads to the following:
+
+```clojure
+(def my-map
+ {:a-key 1
+ :other-key 2})
+```
+
+This can also be done automatically (as part of indentation) by turning on
+`clojure-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
@@ -248,7 +290,7 @@ To highlight entire rich `comment` expression with the comment font face, set
```
By default this is `nil`, so that anything within a `comment` expression is
-highlighted like regular clojure code.
+highlighted like regular Clojure code.
> [!TIP]
>
@@ -260,13 +302,31 @@ highlighted like regular clojure code.
### Highlight markdown syntax in docstrings
-By default markdown syntax is highlighted in the docstrings using
-`markdown_inline` grammar. To disable this feature set
+By default Markdown syntax is highlighted in the docstrings using
+`markdown-inline` grammar. To disable this feature use:
``` emacs-lisp
(setopt clojure-ts-use-markdown-inline nil)
```
+Example of Markdown syntax highlighting:
+
+
+
+### Highlight regular expression syntax
+
+By default syntax inside regex literals is highlighted using
+[regex](https://github.com/tree-sitter/tree-sitter-regex) grammar. To disable
+this feature use:
+
+```emacs-lisp
+(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
@@ -286,7 +346,7 @@ Every new line in the docstrings is indented by
`clojure-ts-docstring-fill-prefix-width` number of spaces (set to 2 by default
which matches the `clojure-mode` settings).
-#### imenu
+### imenu
`clojure-ts-mode` supports various types of definition that can be navigated
using `imenu`, such as:
@@ -299,11 +359,128 @@ using `imenu`, such as:
- class (forms such as `deftype`, `defrecord` and `defstruct`)
- keyword (for example, spec definitions)
+### Integration with `outline-minor-mode`
+
+`clojure-ts-mode` supports two integration variants with
+`outline-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 the
+`imenu` command) as outline headings. To use the second option, use the
+following customization:
+
+```emacs-lisp
+(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:
+
+```clojure
+(->map (assoc {} :key "value") :lock)
+```
+
+After executing `clojure-ts-thread-last-all` will be converted to:
+
+```clojure
+(-> {}
+ (assoc :key "value")
+ (->map :lock))
+```
+
+This behavior can be changed by setting:
+
+```emacs-lisp
+(setopt clojure-ts-thread-all-but-last t)
+```
+
+Then the last expression will not be threaded and the result will be:
+
+```clojure
+(-> (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 of `def`s or `defn`s. Use metadata
+explicitly with setting `clojure-ts-use-metadata-for-defn-privacy` to `t` for
+`defn`s too.
+- `clojure-ts-cycle-conditional`: Change a surrounding conditional form to its
+ negated counterpart, or vice versa (supports `if`/`if-not` and
+ `when`/`when-not`). For `if`/`if-not` also transposes the else and then
+ branches, keeping the semantics the same as before.
+- `clojure-ts-cycle-not`: Add or remove a `not` form 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-list`
+- `clojure-ts-convert-collection-to-quoted-list`
+- `clojure-ts-convert-collection-to-map`
+- `clojure-ts-convert-collection-to-vector`
+- `clojure-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` or
+`defmethod` form. This command also supports functions defined inside forms like
+`letfn`, `defprotol`, `reify` 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.
+
## 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.
+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.
+After installing the package do the following:
- Check the value of `clojure-mode-hook` and copy all relevant hooks to `clojure-ts-mode-hook`.
@@ -314,7 +491,8 @@ After installing the package do the following.
(add-hook 'clojure-ts-mode-hook #'clj-refactor-mode)
```
-- Update `.dir-locals.el` in all of your Clojure projects to activate directory local variables in `clojure-ts-mode`.
+- Update `.dir-locals.el` in all of your Clojure projects to activate directory
+ local variables in `clojure-ts-mode`.
``` emacs-lisp
((clojure-mode
@@ -325,14 +503,14 @@ After installing the package do the following.
## Caveats
-As the TreeSitter Emacs APIs are new and keep evolving there are some
+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 both `prog-mode`
and `clojure-mode` (this is very helpful when dealing with `derived-mode-p` checks)
- Navigation by sexp/lists might work differently on Emacs versions lower
- than 31. Starting with version 31, Emacs uses TreeSitter 'things' settings, if
+ 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
@@ -344,8 +522,8 @@ and `clojure-mode` (this is very helpful when dealing with `derived-mode-p` chec
### What `clojure-mode` features are currently missing?
-As of version 0.2.x, the most obvious missing feature are the various
-refactoring commands in `clojure-mode`.
+As of version 0.4.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?
@@ -367,7 +545,6 @@ Check out [this article](https://metaredux.com/posts/2024/02/19/cider-preliminar
> [!NOTE]
>
> The dynamic indentation feature in CIDER requires clojure-ts-mode 0.3+.
-> Dynamic font-locking currently doesn't work with clojure-ts-mode.
### Does `clojure-ts-mode` work with `inf-clojure`?
diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el
index 971d23d..a4263b4 100644
--- a/clojure-ts-mode.el
+++ b/clojure-ts-mode.el
@@ -7,7 +7,7 @@
;; Maintainer: Bozhidar Batsov
;; URL: http://github.com/clojure-emacs/clojure-ts-mode
;; Keywords: languages clojure clojurescript lisp
-;; Version: 0.3.0
+;; Version: 0.4.0
;; Package-Requires: ((emacs "30.1"))
;; This file is not part of GNU Emacs.
@@ -17,7 +17,7 @@
;; Provides font-lock, indentation, and navigation for the
;; Clojure programming language (http://clojure.org).
-;; For the tree-sitter grammar this mode is based on,
+;; For the Tree-sitter grammar this mode is based on,
;; see https://github.com/sogaiu/tree-sitter-clojure.
;; Using clojure-ts-mode with paredit or smartparens is highly recommended.
@@ -56,6 +56,8 @@
;;; Code:
(require 'treesit)
+(require 'align)
+(require 'subr-x)
(declare-function treesit-parser-create "treesit.c")
(declare-function treesit-node-eq "treesit.c")
@@ -65,14 +67,14 @@
(declare-function treesit-node-child-by-field-name "treesit.c")
(defgroup clojure-ts nil
- "Major mode for editing Clojure code with tree-sitter."
+ "Major mode for editing Clojure code with Tree-sitter."
:prefix "clojure-ts-"
:group 'languages
:link '(url-link :tag "GitHub" "https://github.com/clojure-emacs/clojure-ts-mode")
:link '(emacs-commentary-link :tag "Commentary" "clojure-mode"))
(defconst clojure-ts-mode-version
- "0.3.0"
+ "0.4.0"
"The current version of `clojure-ts-mode'.")
(defcustom clojure-ts-comment-macro-font-lock-body nil
@@ -88,7 +90,7 @@ itself."
:package-version '(clojure-ts-mode . "0.1.3"))
(defcustom clojure-ts-ensure-grammars t
- "When non-nil, ensure required tree-sitter grammars are installed."
+ "When non-nil, ensure required Tree-sitter grammars are installed."
:safe #'booleanp
:type 'boolean
:package-version '(clojure-ts-mode . "0.2.0"))
@@ -120,12 +122,132 @@ double quotes on the third column."
:safe #'booleanp
:package-version '(clojure-ts-mode . "0.2.3"))
+(defcustom clojure-ts-use-regex-parser t
+ "When non-nil, use separate grammar to highlight regex syntax."
+ :type 'boolean
+ :safe #'booleanp
+ :package-version '(clojure-ts-mode . "0.4"))
+
(defcustom clojure-ts-auto-remap t
"When non-nil, redirect all `clojure-mode' buffers to `clojure-ts-mode'."
:safe #'booleanp
:type 'boolean
:package-version '(clojure-ts-mode . "0.3"))
+(defcustom clojure-ts-outline-variant 'comments
+ "Determines how `clojure-ts-mode' integrates with `outline-minor-mode'.
+
+If set to the symbol `comments', then top-level comments starting with
+three or more semicolons will be treated as outline headings. If set to
+`imenu', then def-like forms are treated as outline headings."
+ :safe #'symbolp
+ :type '(choice (const :tag "Use special comments" comments)
+ (const :tag "Use imenu" imenu))
+ :package-version '(clojure-ts-mode . "0.4"))
+
+(defcustom clojure-ts-refactor-map-prefix "C-c C-r"
+ "Clojure refactor keymap prefix."
+ :type 'string
+ :package-version '(clojure-ts-mode . "0.4"))
+
+(defcustom clojure-ts-thread-all-but-last nil
+ "Non-nil means do not thread the last expression.
+
+This means that `clojure-ts-thread-first-all' and
+`clojure-ts-thread-last-all' not thread the deepest sexp inside the
+current sexp."
+ :package-version '(clojure-ts-mode . "0.4")
+ :safe #'booleanp
+ :type 'boolean)
+
+(defcustom clojure-ts-use-metadata-for-defn-privacy nil
+ "If nil, `clojure-ts-cycle-privacy' will use (defn- f []).
+
+If t, it will use (defn ^:private f [])."
+ :package-version '(clojure-ts-mode . "0.4")
+ :safe #'booleanp
+ :type 'boolean)
+
+(defcustom clojure-ts-align-reader-conditionals nil
+ "Whether to align reader conditionals, as if they were maps."
+ :package-version '(clojure-ts-mode . "0.4")
+ :safe #'booleanp
+ :type 'boolean)
+
+(defcustom clojure-ts-align-binding-forms
+ '("let"
+ "when-let"
+ "when-some"
+ "if-let"
+ "if-some"
+ "binding"
+ "loop"
+ "doseq"
+ "for"
+ "with-open"
+ "with-local-vars"
+ "with-redefs"
+ "clojure.core/let"
+ "clojure.core/when-let"
+ "clojure.core/when-some"
+ "clojure.core/if-let"
+ "clojure.core/if-some"
+ "clojure.core/binding"
+ "clojure.core/loop"
+ "clojure.core/doseq"
+ "clojure.core/for"
+ "clojure.core/with-open"
+ "clojure.core/with-local-vars"
+ "clojure.core/with-redefs")
+ "List of strings matching forms that have binding forms."
+ :package-version '(clojure-ts-mode . "0.4")
+ :safe #'listp
+ :type '(repeat string))
+
+(defconst clojure-ts--align-separator-newline-regexp "^ *$")
+
+(defcustom clojure-ts-align-separator clojure-ts--align-separator-newline-regexp
+ "Separator passed to `align-region' when performing vertical alignment."
+ :package-version '(clojure-ts-mode . "0.4")
+ :type `(choice (const :tag "Make blank lines prevent vertical alignment from happening."
+ ,clojure-ts--align-separator-newline-regexp)
+ (other :tag "Allow blank lines to happen within a vertically-aligned expression."
+ entire)))
+
+(defcustom clojure-ts-align-cond-forms
+ '("condp"
+ "cond"
+ "cond->"
+ "cond->>"
+ "case"
+ "are"
+ "clojure.core/condp"
+ "clojure.core/cond"
+ "clojure.core/cond->"
+ "clojure.core/cond->>"
+ "clojure.core/case"
+ "clojure.core/are")
+ "List of strings identifying cond-like forms."
+ :package-version '(clojure-ts-mode . "0.4")
+ :safe #'listp
+ :type '(repeat string))
+
+(defcustom clojure-ts-align-forms-automatically nil
+ "If non-nil, vertically align some forms automatically.
+
+Automatically means it is done as part of indenting code. This applies
+to binding forms (`clojure-ts-align-binding-forms'), to cond
+forms (`clojure-ts-align-cond-forms') and to map literals. For
+instance, selecting a map a hitting
+\\`\\[indent-for-tab-command]' will align the
+values like this:
+
+{:some-key 10
+ :key2 20}"
+ :package-version '(clojure-ts-mode . "0.4")
+ :safe #'booleanp
+ :type 'boolean)
+
(defvar clojure-ts-mode-remappings
'((clojure-mode . clojure-ts-mode)
(clojurescript-mode . clojure-ts-clojurescript-mode)
@@ -342,17 +464,37 @@ if a third argument (the value) is provided.
:*)
(:match ,clojure-ts--interface-def-symbol-regexp @_def_symbol))))
-(defvar clojure-ts--treesit-range-settings
- (treesit-range-rules
- :embed 'markdown-inline
- :host 'clojure
- :local t
- (clojure-ts--docstring-query '@capture)))
+(defun clojure-ts--treesit-range-settings (use-markdown-inline use-regex)
+ "Return value for `treesit-range-settings' for `clojure-ts-mode'.
-(defun clojure-ts--font-lock-settings (markdown-available)
+When USE-MARKDOWN-INLINE is non-nil, include range settings for
+markdown-inline parser.
+
+When USE-REGEX is non-nil, include range settings for regex parser."
+ (append
+ (when use-markdown-inline
+ (treesit-range-rules
+ :embed 'markdown-inline
+ :host 'clojure
+ :offset '(1 . -1)
+ :local t
+ (clojure-ts--docstring-query '@capture)))
+ (when use-regex
+ (treesit-range-rules
+ :embed 'regex
+ :host 'clojure
+ :offset '(2 . -1)
+ :local t
+ '((regex_lit) @capture)))))
+
+(defun clojure-ts--font-lock-settings (markdown-available regex-available)
"Return font lock settings suitable for use in `treesit-font-lock-settings'.
+
When MARKDOWN-AVAILABLE is non-nil, includes rules for highlighting docstrings
-with the markdown-inline grammar."
+with the markdown-inline grammar.
+
+When REGEX-AVAILABLE is non-nil, includes rules for highlighting regex
+literals with regex grammar."
(append
(treesit-font-lock-rules
:feature 'string
@@ -396,6 +538,13 @@ with the markdown-inline grammar."
(:equal "clojure.core" @ns))
name: (sym_name) @font-lock-keyword-face))
(:match ,clojure-ts--builtin-symbol-regexp @font-lock-keyword-face))
+ ((anon_fn_lit meta: _ :* :anchor (sym_lit !namespace name: (sym_name) @font-lock-keyword-face))
+ (:match ,clojure-ts--builtin-symbol-regexp @font-lock-keyword-face))
+ ((anon_fn_lit meta: _ :* :anchor
+ (sym_lit namespace: ((sym_ns) @ns
+ (:equal "clojure.core" @ns))
+ name: (sym_name) @font-lock-keyword-face))
+ (:match ,clojure-ts--builtin-symbol-regexp @font-lock-keyword-face))
((sym_name) @font-lock-builtin-face
(:match ,clojure-ts--builtin-dynamic-var-regexp @font-lock-builtin-face)))
@@ -525,6 +674,44 @@ with the markdown-inline grammar."
(inline_link (link_destination) @font-lock-constant-face)
(shortcut_link (link_text) @link)])))
+ (when regex-available
+ ;; Queries are adapted from
+ ;; https://github.com/tree-sitter/tree-sitter-regex/blob/v0.24.3/queries/highlights.scm.
+ (treesit-font-lock-rules
+ :feature 'regex
+ :language 'regex
+ :override t
+ '((["("
+ ")"
+ "(?"
+ "(?:"
+ "(?<"
+ "(?P<"
+ "(?P="
+ ">"
+ "["
+ "]"
+ "{"
+ "}"
+ "[:"
+ ":]"] @font-lock-regexp-grouping-construct)
+ (["*"
+ "+"
+ "?"
+ "|"
+ "="
+ "!"] @font-lock-property-name-face)
+ ((group_name) @font-lock-variable-name-face)
+ ((count_quantifier
+ [(decimal_digits) @font-lock-number-face
+ "," @font-lock-delimiter-face]))
+ ((flags) @font-lock-constant-face)
+ ((character_class
+ ["^" @font-lock-escape-face
+ (class_range "-" @font-lock-escape-face)]))
+ ((identity_escape) @font-lock-builtin-face)
+ ([(start_assertion) (end_assertion)] @font-lock-constant-face))))
+
(treesit-font-lock-rules
:feature 'quote
:language 'clojure
@@ -570,6 +757,18 @@ with the markdown-inline grammar."
"Return non-nil if NODE is a Clojure list."
(string-equal "list_lit" (treesit-node-type node)))
+(defun clojure-ts--vec-node-p (node)
+ "Return non-nil if NODE is a Clojure vector."
+ (string-equal "vec_lit" (treesit-node-type node)))
+
+(defun clojure-ts--anon-fn-node-p (node)
+ "Return non-nil if NODE is a Clojure function literal."
+ (string-equal "anon_fn_lit" (treesit-node-type node)))
+
+(defun clojure-ts--opening-paren-node-p (node)
+ "Return non-nil if NODE is an opening paren."
+ (string-equal "(" (treesit-node-text node)))
+
(defun clojure-ts--symbol-node-p (node)
"Return non-nil if NODE is a Clojure symbol."
(string-equal "sym_lit" (treesit-node-type node)))
@@ -768,6 +967,20 @@ Includes a dispatch value when applicable (defmethods)."
By default `treesit-defun-name-function' is used to extract definition names.
See `clojure-ts--standard-definition-node-name' for the implementation used.")
+;;; Outline settings
+
+(defun clojure-ts--outline-predicate (node)
+ "Return TRUE if NODE is an outline heading comment."
+ (and (string= (treesit-node-type node) "comment")
+ (string-match-p "^\\(?:;;;;* \\).*" (treesit-node-text node))))
+
+(defun clojure-ts--outline-level ()
+ "Return the current level of the outline heading at point."
+ (let* ((node (treesit-outline--at-point))
+ (node-text (treesit-node-text node)))
+ (string-match ";;\\(;+\\) " node-text)
+ (- (match-end 1) (match-beginning 1))))
+
(defcustom clojure-ts-indent-style 'semantic
"Automatic indentation style to use when mode `clojure-ts-mode' is run.
@@ -1025,6 +1238,18 @@ If NS is defined, then the fully qualified symbol is passed to
(seq-sort (lambda (spec1 _spec2)
(equal (car spec1) :block)))))))))
+(defun clojure-ts--find-semantic-rules-for-node (node)
+ "Return a list of semantic rules for NODE."
+ (let* ((first-child (clojure-ts--node-child-skip-metadata node 0))
+ (symbol-name (clojure-ts--named-node-text first-child))
+ (symbol-namespace (clojure-ts--node-namespace-text first-child)))
+ (or (clojure-ts--dynamic-indent-for-symbol symbol-name symbol-namespace)
+ (alist-get symbol-name
+ clojure-ts--semantic-indent-rules-cache
+ nil
+ nil
+ #'equal))))
+
(defun clojure-ts--find-semantic-rule (node parent current-depth)
"Return a suitable indentation rule for NODE, considering the CURRENT-DEPTH.
@@ -1034,16 +1259,8 @@ syntax tree and recursively attempts to find a rule, incrementally
increasing the CURRENT-DEPTH. If a rule is not found upon reaching the
root of the syntax tree, it returns nil. A rule is considered a match
only if the CURRENT-DEPTH matches the rule's required depth."
- (let* ((first-child (clojure-ts--node-child-skip-metadata parent 0))
- (symbol-name (clojure-ts--named-node-text first-child))
- (symbol-namespace (clojure-ts--node-namespace-text first-child))
- (idx (- (treesit-node-index node) 2)))
- (if-let* ((rule-set (or (clojure-ts--dynamic-indent-for-symbol symbol-name symbol-namespace)
- (alist-get symbol-name
- clojure-ts--semantic-indent-rules-cache
- nil
- nil
- #'equal))))
+ (let* ((idx (- (treesit-node-index node) 2)))
+ (if-let* ((rule-set (clojure-ts--find-semantic-rules-for-node parent)))
(if (zerop current-depth)
(let ((rule (car rule-set)))
(if (equal (car rule) :block)
@@ -1061,7 +1278,9 @@ only if the CURRENT-DEPTH matches the rule's required depth."
(or (null rule-idx)
(equal rule-idx idx))))))
(seq-first)))
- (when-let* ((new-parent (treesit-node-parent parent)))
+ ;; Let's go no more than 3 levels up to avoid performance degradation.
+ (when-let* (((< current-depth 3))
+ (new-parent (treesit-node-parent parent)))
(clojure-ts--find-semantic-rule parent
new-parent
(1+ current-depth))))))
@@ -1073,7 +1292,8 @@ PARENT not should be a list. If first symbol in the expression has an
indentation rule in `clojure-ts--semantic-indent-rules-defaults' or
`clojure-ts-semantic-indent-rules' check if NODE should be indented
according to the rule. If NODE is nil, use next node after BOL."
- (and (clojure-ts--list-node-p parent)
+ (and (or (clojure-ts--list-node-p parent)
+ (clojure-ts--anon-fn-node-p parent))
(let* ((first-child (clojure-ts--node-child-skip-metadata parent 0)))
(when-let* ((rule (clojure-ts--find-semantic-rule node parent 0)))
(and (not (clojure-ts--match-with-metadata node))
@@ -1089,12 +1309,13 @@ according to the rule. If NODE is nil, use next node after BOL."
(defun clojure-ts--match-function-call-arg (node parent _bol)
"Match NODE if PARENT is a list expressing a function or macro call."
- (and (clojure-ts--list-node-p parent)
+ (and (or (clojure-ts--list-node-p parent)
+ (clojure-ts--anon-fn-node-p parent))
;; Can the following two clauses be replaced by checking indexes?
;; Does the second child exist, and is it not equal to the current node?
- (treesit-node-child parent 1 t)
- (not (treesit-node-eq (treesit-node-child parent 1 t) node))
- (let ((first-child (treesit-node-child parent 0 t)))
+ (clojure-ts--node-child-skip-metadata parent 1)
+ (not (treesit-node-eq (clojure-ts--node-child-skip-metadata parent 1) node))
+ (let ((first-child (clojure-ts--node-child-skip-metadata parent 0)))
(or (clojure-ts--symbol-node-p first-child)
(clojure-ts--keyword-node-p first-child)
(clojure-ts--var-node-p first-child)))))
@@ -1108,7 +1329,8 @@ according to the rule. If NODE is nil, use next node after BOL."
"Match NODE if it is an argument to a PARENT threading macro."
;; We want threading macros to indent 2 only if the ->> is on it's own line.
;; If not, then align function arg.
- (and (clojure-ts--list-node-p parent)
+ (and (or (clojure-ts--list-node-p parent)
+ (clojure-ts--anon-fn-node-p parent))
(let ((first-child (treesit-node-child parent 0 t)))
(clojure-ts--symbol-matches-p
clojure-ts--threading-macro
@@ -1159,7 +1381,7 @@ according to the rule. If NODE is nil, use next node after BOL."
(and prev-sibling
(clojure-ts--metadata-node-p prev-sibling))))
-(defun clojure-ts--anchor-parent-skip-metadata (_node parent _bol)
+(defun clojure-ts--anchor-parent-opening-paren (_node parent _bol)
"Return position of PARENT start for NODE.
If PARENT has optional metadata we skip it and return starting position
@@ -1167,11 +1389,9 @@ of the first child's opening paren.
NOTE: This serves as an anchor function to resolve an indentation issue
for forms with type hints."
- (let ((first-child (treesit-node-child parent 0 t)))
- (if (clojure-ts--metadata-node-p first-child)
- ;; We don't need named node here
- (treesit-node-start (treesit-node-child parent 1))
- (treesit-node-start parent))))
+ (thread-first parent
+ (treesit-search-subtree #'clojure-ts--opening-paren-node-p nil t 1)
+ (treesit-node-start)))
(defun clojure-ts--match-collection-item-with-metadata (node-type)
"Return a matcher for a collection item with metadata by NODE-TYPE.
@@ -1183,24 +1403,30 @@ if NODE has metadata and its parent has type NODE-TYPE."
(treesit-node-type
(clojure-ts--node-with-metadata-parent node)))))
+(defun clojure-ts--anchor-nth-sibling (n)
+ "Return the start of the Nth child of PARENT skipping metadata."
+ (lambda (_n parent &rest _)
+ (treesit-node-start
+ (clojure-ts--node-child-skip-metadata parent n))))
+
(defun clojure-ts--semantic-indent-rules ()
- "Return a list of indentation rules for `treesit-simple-indent-rules'."
+ "Return a list of indentation rules for `treesit-simple-indent-rules'.
+
+NOTE: All built-in matchers (such as `parent-is' etc) expect a node type
+regex. Therefore, if the string map_lit is used, it will incorrectly
+match both map_lit and ns_map_lit. To prevent this, more precise
+regexes with anchors matching the beginning and end of the line are
+used."
`((clojure
- ((parent-is "source") parent-bol 0)
+ ((parent-is "^source$") parent-bol 0)
(clojure-ts--match-docstring parent 0)
- ;; https://guide.clojure.style/#body-indentation
- (clojure-ts--match-form-body clojure-ts--anchor-parent-skip-metadata 2)
- ;; https://guide.clojure.style/#threading-macros-alignment
- (clojure-ts--match-threading-macro-arg prev-sibling 0)
- ;; https://guide.clojure.style/#vertically-align-fn-args
- (clojure-ts--match-function-call-arg (nth-sibling 2 nil) 0)
;; Collections items with metadata.
;;
;; This should be before `clojure-ts--match-with-metadata', otherwise they
;; will never be matched.
- (,(clojure-ts--match-collection-item-with-metadata "vec_lit") grand-parent 1)
- (,(clojure-ts--match-collection-item-with-metadata "map_lit") grand-parent 1)
- (,(clojure-ts--match-collection-item-with-metadata "set_lit") grand-parent 2)
+ (,(clojure-ts--match-collection-item-with-metadata "^vec_lit$") grand-parent 1)
+ (,(clojure-ts--match-collection-item-with-metadata "^map_lit$") grand-parent 1)
+ (,(clojure-ts--match-collection-item-with-metadata "^set_lit$") grand-parent 2)
;;
;; If we enable this rule for lists, it will break many things.
;; (,(clojure-ts--match-collection-item-with-metadata "list_lit") grand-parent 1)
@@ -1208,10 +1434,21 @@ if NODE has metadata and its parent has type NODE-TYPE."
;; All other forms with metadata.
(clojure-ts--match-with-metadata parent 0)
;; Literal Sequences
- ((parent-is "list_lit") parent 1) ;; https://guide.clojure.style/#one-space-indent
- ((parent-is "vec_lit") parent 1) ;; https://guide.clojure.style/#bindings-alignment
- ((parent-is "map_lit") parent 1) ;; https://guide.clojure.style/#map-keys-alignment
- ((parent-is "set_lit") parent 2))))
+ ((parent-is "^vec_lit$") parent 1) ;; https://guide.clojure.style/#bindings-alignment
+ ((parent-is "^map_lit$") parent 1) ;; https://guide.clojure.style/#map-keys-alignment
+ ((parent-is "^set_lit$") parent 2)
+ ((parent-is "^splicing_read_cond_lit$") parent 4)
+ ((parent-is "^read_cond_lit$") parent 3)
+ ((parent-is "^tagged_or_ctor_lit$") parent 0)
+ ((parent-is "^ns_map_lit$") (nth-sibling 2) 1)
+ ;; https://guide.clojure.style/#body-indentation
+ (clojure-ts--match-form-body clojure-ts--anchor-parent-opening-paren 2)
+ ;; https://guide.clojure.style/#threading-macros-alignment
+ (clojure-ts--match-threading-macro-arg prev-sibling 0)
+ ;; https://guide.clojure.style/#vertically-align-fn-args
+ (clojure-ts--match-function-call-arg ,(clojure-ts--anchor-nth-sibling 1) 0)
+ ;; https://guide.clojure.style/#one-space-indent
+ ((parent-is "^list_lit$") parent 1))))
(defun clojure-ts--configured-indent-rules ()
"Gets the configured choice of indent rules."
@@ -1245,6 +1482,27 @@ If JUSTIFY is non-nil, justify as well as fill the paragraph."
(fill-paragraph justify)))
t))
+(defun clojure-ts--list-node-sym-text (node &optional include-anon-fn-lit)
+ "Return text of the first child of the NODE if NODE is a list.
+
+Return nil if the NODE is not a list or if the first child is not a
+symbol. Optionally if INCLUDE-ANON-FN-LIT is non-nil, return the text
+of the first symbol of a functional literal NODE."
+ (when (or (clojure-ts--list-node-p node)
+ (and include-anon-fn-lit
+ (clojure-ts--anon-fn-node-p node)))
+ (when-let* ((first-child (clojure-ts--node-child-skip-metadata node 0))
+ ((clojure-ts--symbol-node-p first-child)))
+ (clojure-ts--named-node-text first-child))))
+
+(defun clojure-ts--list-node-sym-match-p (node regex &optional include-anon-fn-lit)
+ "Return TRUE if NODE is a list and its first symbol matches the REGEX.
+
+Optionally if INCLUDE-ANON-FN-LIT is TRUE, perform the same check for a
+function literal."
+ (when-let* ((sym-text (clojure-ts--list-node-sym-text node include-anon-fn-lit)))
+ (string-match-p regex sym-text)))
+
(defconst clojure-ts--sexp-nodes
'("#_" ;; transpose-sexp near a discard macro moves it around.
"num_lit" "sym_lit" "kwd_lit" "nil_lit" "bool_lit"
@@ -1254,7 +1512,8 @@ If JUSTIFY is non-nil, justify as well as fill the paragraph."
"var_quoting_lit" "sym_val_lit" "evaling_lit"
"tagged_or_ctor_lit" "splicing_read_cond_lit"
"derefing_lit" "quoting_lit" "syn_quoting_lit"
- "unquote_splicing_lit" "unquoting_lit")
+ "unquote_splicing_lit" "unquoting_lit"
+ "dis_expr")
"A regular expression that matches nodes that can be treated as s-expressions.")
(defconst clojure-ts--list-nodes
@@ -1262,6 +1521,19 @@ If JUSTIFY is non-nil, justify as well as fill the paragraph."
"map_lit" "ns_map_lit" "vec_lit" "set_lit")
"A regular expression that matches nodes that can be treated as lists.")
+(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")
+ eol)))
+
(defconst clojure-ts--markdown-inline-sexp-nodes
'("inline_link" "full_reference_link" "collapsed_reference_link"
"uri_autolink" "email_autolink" "shortcut_link" "image"
@@ -1272,15 +1544,891 @@ If JUSTIFY is non-nil, justify as well as fill the paragraph."
`((clojure
(sexp ,(regexp-opt clojure-ts--sexp-nodes))
(list ,(regexp-opt clojure-ts--list-nodes))
- (text ,(regexp-opt '("comment"))))
+ (text ,(regexp-opt '("comment")))
+ (defun ,#'clojure-ts--defun-node-p))
(when clojure-ts-use-markdown-inline
(markdown-inline
(sexp ,(regexp-opt clojure-ts--markdown-inline-sexp-nodes))))))
+;;; Vertical alignment
+
+(defun clojure-ts--beginning-of-defun-pos ()
+ "Return the point that represents the beginning of the current defun."
+ (treesit-node-start (treesit-defun-at-point)))
+
+(defun clojure-ts--end-of-defun-pos ()
+ "Return the point that represends the end of the current defun."
+ (treesit-node-end (treesit-defun-at-point)))
+
+(defun clojure-ts--search-whitespace-after-next-sexp (root-node bound)
+ "Move the point after all whitespace following the next s-expression.
+
+Set match data group 1 to this region of whitespace and return the
+point.
+
+To move over the next s-expression, fetch the next node after the
+current cursor position that is a direct child of ROOT-NODE and navigate
+to its end. The most complex aspect here is handling nodes with
+metadata. Some forms are represented in the syntax tree as a single
+s-expression (for example, ^long my-var or ^String (str \"Hello\"
+\"world\")), while other forms are two separate s-expressions (for
+example, ^long 123 or ^String \"Hello\"). Expressions with two nodes
+share some common features:
+
+- The top-level node type is usually sym_lit
+
+- They do not have value children, or they have an empty name.
+
+Regular expression and syntax analysis code is borrowed from
+`clojure-mode.'
+
+BOUND bounds the whitespace search."
+ (unwind-protect
+ (let ((regex "\\([,\s\t]*\\)\\(;+.*\\)?"))
+ ;; If we're on an empty line, we should return match, otherwise
+ ;; `clojure-ts-align-separator' setting won't work.
+ (if (and (bolp) (looking-at-p "[[:blank:]]*$"))
+ (progn
+ (search-forward-regexp regex bound)
+ (point))
+ (when-let* ((cur-sexp (treesit-node-first-child-for-pos root-node (point) t)))
+ (goto-char (treesit-node-start cur-sexp))
+ (if (and (string= "sym_lit" (treesit-node-type cur-sexp))
+ (clojure-ts--metadata-node-p (treesit-node-child cur-sexp 0 t))
+ (and (not (treesit-node-child-by-field-name cur-sexp "value"))
+ (string-empty-p (clojure-ts--named-node-text cur-sexp))))
+ (treesit-end-of-thing 'sexp 2 'restricted)
+ (treesit-end-of-thing 'sexp 1 'restricted))
+ (when (looking-at-p ",")
+ (forward-char))
+ ;; Move past any whitespace or comment.
+ (search-forward-regexp regex bound)
+ (pcase (syntax-after (point))
+ ;; End-of-line, try again on next line.
+ (`(12) (progn
+ (forward-char 1)
+ (clojure-ts--search-whitespace-after-next-sexp root-node bound)))
+ ;; Closing paren, stop here.
+ (`(5 . ,_) nil)
+ ;; Anything else is something to align.
+ (_ (point))))))
+ (when (and bound (> (point) bound))
+ (goto-char bound))))
+
+(defun clojure-ts--region-node (beg end)
+ "Return the smallest node that covers buffer positions BEG to END."
+ (let* ((root-node (treesit-buffer-root-node 'clojure)))
+ (treesit-node-descendant-for-range root-node beg end t)))
+
+(defun clojure-ts--node-from-sexp-data (beg end sexp)
+ "Return updated node using SEXP data in the region between BEG and END."
+ (let* ((new-region-node (clojure-ts--region-node beg end))
+ (sexp-beg (marker-position (plist-get sexp :beg-marker)))
+ (sexp-end (marker-position (plist-get sexp :end-marker))))
+ (treesit-node-descendant-for-range new-region-node
+ sexp-beg
+ sexp-end
+ t)))
+
+(defun clojure-ts--get-nodes-to-align (beg end)
+ "Return a plist of nodes data for alignment.
+
+The search is limited by BEG, END.
+
+Possible node types are: map, bindings-vec, cond or read-cond.
+
+The returned value is a list of property lists. Each property list
+includes `:sexp-type', `:node', `:beg-marker', and `:end-marker'.
+Markers are necessary to fetch the same nodes after their boundaries
+have changed."
+ ;; By default `treesit-query-capture' captures all nodes that cross the range.
+ ;; We need to restrict it to only nodes inside of the range.
+ (let* ((region-node (clojure-ts--region-node beg end))
+ (query (treesit-query-compile 'clojure
+ (append
+ `(((map_lit) @map)
+ ((ns_map_lit) @ns-map)
+ ((list_lit
+ ((sym_lit) @sym
+ (:match ,(clojure-ts-symbol-regexp clojure-ts-align-binding-forms) @sym))
+ (vec_lit) @bindings-vec))
+ ((list_lit
+ ((sym_lit) @sym
+ (:match ,(clojure-ts-symbol-regexp clojure-ts-align-cond-forms) @sym)))
+ @cond)
+ ((anon_fn_lit
+ ((sym_lit) @sym
+ (:match ,(clojure-ts-symbol-regexp clojure-ts-align-binding-forms) @sym))
+ (vec_lit) @bindings-vec))
+ ((anon_fn_lit
+ ((sym_lit) @sym
+ (:match ,(clojure-ts-symbol-regexp clojure-ts-align-cond-forms) @sym)))
+ @cond))
+ (when clojure-ts-align-reader-conditionals
+ '(((read_cond_lit) @read-cond)
+ ((splicing_read_cond_lit) @read-cond)))))))
+ (thread-last (treesit-query-capture region-node query beg end)
+ (seq-remove (lambda (elt) (eq (car elt) 'sym)))
+ ;; When first node is reindented, all other nodes become
+ ;; outdated. Executing the entire query everytime is very
+ ;; expensive, instead we use markers for every captured node to
+ ;; retrieve only a single node later.
+ (seq-map (lambda (elt)
+ (let* ((sexp-type (car elt))
+ (node (cdr elt))
+ (beg-marker (copy-marker (treesit-node-start node) t))
+ (end-marker (copy-marker (treesit-node-end node))))
+ (list :sexp-type sexp-type
+ :node node
+ :beg-marker beg-marker
+ :end-marker end-marker)))))))
+
+(defun clojure-ts--point-to-align-position (sexp-type node)
+ "Move point to the appropriate position to align NODE.
+
+For NODE with SEXP-TYPE map or bindings-vec, the appropriate
+position is after the first opening brace.
+
+For NODE with SEXP-TYPE cond, we need to skip the first symbol and the
+subsequent special arguments based on block indentation rules."
+ (goto-char (treesit-node-start node))
+ (when-let* ((cur-sexp (treesit-node-first-child-for-pos node (point) t)))
+ (goto-char (treesit-node-start cur-sexp))
+ ;; For namespaced maps we need to skip the namespace, which is the first
+ ;; nested sexp.
+ (when (equal sexp-type 'ns-map)
+ (treesit-beginning-of-thing 'sexp -1 'nested))
+ ;; For cond forms we need to skip first n + 1 nodes according to block
+ ;; indentation rules. First node to skip is the symbol itself.
+ (when (equal sexp-type 'cond)
+ (if-let* ((rule-set (clojure-ts--find-semantic-rules-for-node node))
+ (rule (car rule-set))
+ ((equal (car rule) :block)))
+ (treesit-beginning-of-thing 'sexp (1- (- (cadr rule))) 'restrict)
+ (treesit-beginning-of-thing 'sexp -1)))))
+
+(defun clojure-ts-align (beg end)
+ "Vertically align the contents of the sexp around point.
+
+If region is active, align it. Otherwise, align everything in the
+current \"top-level\" sexp. When called from lisp code align everything
+between BEG and END."
+ (interactive (if (use-region-p)
+ (list (region-beginning) (region-end))
+ (save-excursion
+ (if (not (treesit-defun-at-point))
+ (user-error "No defun at point")
+ (let ((start (clojure-ts--beginning-of-defun-pos))
+ (end (clojure-ts--end-of-defun-pos)))
+ (list start end))))))
+ (setq end (copy-marker end))
+ (let* ((sexps-to-align (clojure-ts--get-nodes-to-align beg (marker-position end)))
+ ;; We have to disable it here to avoid endless recursion.
+ (clojure-ts-align-forms-automatically nil))
+ (save-excursion
+ (indent-region beg end)
+ (dolist (sexp sexps-to-align)
+ ;; After reindenting a node, all other nodes in the `sexps-to-align'
+ ;; list become outdated, so we need to fetch updated nodes for every
+ ;; iteration.
+ (let* ((node (clojure-ts--node-from-sexp-data beg (marker-position end) sexp))
+ (sexp-type (plist-get sexp :sexp-type))
+ (node-end (treesit-node-end node)))
+ (clojure-ts--point-to-align-position sexp-type node)
+ (align-region (point) node-end nil
+ `((clojure-align (regexp . ,(lambda (&optional bound _noerror)
+ (let ((updated-node (clojure-ts--node-from-sexp-data beg (marker-position end) sexp)))
+ (clojure-ts--search-whitespace-after-next-sexp updated-node bound))))
+ (group . 1)
+ (separate . ,clojure-ts-align-separator)
+ (repeat . t)))
+ nil)
+ ;; After every iteration we have to re-indent the s-expression,
+ ;; otherwise some can be indented inconsistently.
+ (indent-region (marker-position (plist-get sexp :beg-marker))
+ (plist-get sexp :end-marker))))
+ ;; If `clojure-ts-align-separator' is used, `align-region' leaves trailing
+ ;; whitespaces on empty lines.
+ (delete-trailing-whitespace beg (marker-position end)))))
+
+(defun clojure-ts-indent-region (beg end)
+ "Like `indent-region', but also maybe align forms.
+
+Forms between BEG and END are aligned according to
+`clojure-ts-align-forms-automatically'."
+ (prog1 (let ((indent-region-function #'treesit-indent-region))
+ (indent-region beg end))
+ (when clojure-ts-align-forms-automatically
+ (clojure-ts-align beg end))))
+
+;;; Refactoring
+
+(defun clojure-ts--parent-until (pred)
+ "Return the closest parent of node at point that satisfies PRED."
+ (when-let* ((node-at-point (treesit-node-at (point) 'clojure t)))
+ (treesit-parent-until node-at-point pred t)))
+
+(defun clojure-ts--search-list-form-at-point (sym-regex &optional include-anon-fn-lit)
+ "Return the list node at point which first symbol matches SYM-REGEX.
+
+If INCLUDE-ANON-FN-LIT is non-nil, this function may also return a
+functional literal node."
+ (clojure-ts--parent-until
+ (lambda (node)
+ (clojure-ts--list-node-sym-match-p node sym-regex include-anon-fn-lit))))
+
+(defun clojure-ts--threading-sexp-node ()
+ "Return list node at point which is a threading expression."
+ (clojure-ts--search-list-form-at-point (rx bol (* "some") "->" (* ">") eol) t))
+
+(defun clojure-ts--delete-and-extract-sexp ()
+ "Delete the surrounding sexp and return it."
+ (let* ((sexp-node (treesit-thing-at-point 'sexp 'nested))
+ (result (treesit-node-text sexp-node)))
+ (delete-region (treesit-node-start sexp-node)
+ (treesit-node-end sexp-node))
+ result))
+
+(defun clojure-ts--ensure-parens-around-function-name ()
+ "Insert parens around function name if necessary."
+ (unless (string= (treesit-node-text (treesit-node-at (point))) "(")
+ (insert-parentheses 1)
+ (backward-up-list)))
+
+(defun clojure-ts--multiline-sexp-p ()
+ "Return TRUE if s-expression at point is multiline."
+ (let ((sexp (treesit-thing-at-point 'sexp 'nested)))
+ (not (= (line-number-at-pos (treesit-node-start sexp))
+ (line-number-at-pos (treesit-node-end sexp))))))
+
+(defun clojure-ts--unwind-thread-first ()
+ "Unwind a thread first macro once."
+ (let* ((threading-sexp (clojure-ts--threading-sexp-node))
+ (first-child-start (thread-first threading-sexp
+ (treesit-node-child 0 t)
+ (treesit-node-start)
+ (copy-marker))))
+ (save-excursion
+ (goto-char first-child-start)
+ (treesit-beginning-of-thing 'sexp -1)
+ (let ((contents (clojure-ts--delete-and-extract-sexp)))
+ (when (looking-at-p " *\n")
+ (join-line 'following))
+ (just-one-space)
+ (goto-char first-child-start)
+ (treesit-beginning-of-thing 'sexp -1)
+ (let ((multiline-p (clojure-ts--multiline-sexp-p)))
+ (clojure-ts--ensure-parens-around-function-name)
+ (down-list)
+ (forward-sexp)
+ (cond
+ ((and multiline-p (looking-at-p " *\n"))
+ (insert "\n" contents))
+ (multiline-p (insert " " contents "\n"))
+ (t (insert " " contents))))))))
+
+(defun clojure-ts--unwind-thread-last ()
+ "Unwind a thread last macro once."
+ (let* ((threading-sexp (clojure-ts--threading-sexp-node))
+ (first-child-start (thread-first threading-sexp
+ (treesit-node-child 0 t)
+ (treesit-node-start)
+ (copy-marker))))
+ (save-excursion
+ (goto-char first-child-start)
+ (treesit-beginning-of-thing 'sexp -1)
+ (let ((contents (clojure-ts--delete-and-extract-sexp)))
+ (when (looking-at-p " *\n")
+ (join-line 'following))
+ (just-one-space)
+ (goto-char first-child-start)
+ (treesit-beginning-of-thing 'sexp -1)
+ (let ((multiline-p (clojure-ts--multiline-sexp-p)))
+ (clojure-ts--ensure-parens-around-function-name)
+ (forward-list)
+ (down-list -1)
+ (when multiline-p
+ (insert "\n"))
+ (insert " " contents))))))
+
+(defun clojure-ts--node-threading-p (node)
+ "Return non-nil if NODE is a threading macro s-expression."
+ (and (or (clojure-ts--list-node-p node)
+ (clojure-ts--anon-fn-node-p node))
+ (let ((first-child (treesit-node-child node 0 t)))
+ (clojure-ts--symbol-matches-p clojure-ts--threading-macro first-child))))
+
+(defun clojure-ts--skip-first-child (parent)
+ "Move point to the beginning of the first child of the PARENT node."
+ (thread-first parent
+ (treesit-node-child 1 t)
+ (treesit-node-start)
+ (goto-char)))
+
+(defun clojure-ts--nothing-more-to-unwind ()
+ "Return TRUE if threading expression at point has only one argument."
+ (let ((threading-sexp (clojure-ts--threading-sexp-node)))
+ (save-excursion
+ (clojure-ts--skip-first-child threading-sexp)
+ (not (treesit-end-of-thing 'sexp 2 'restricted)))))
+
+(defun clojure-ts--raise-sexp ()
+ "Raise current sexp one level higher up the tree.
+
+The built-in `raise-sexp' function doesn't work well with a few Clojure
+nodes (function literals, expressions with metadata etc.), it loses some
+parenthesis."
+ (when-let* ((sexp-node (treesit-thing-at (point) 'sexp))
+ (beg (thread-first sexp-node
+ (clojure-ts--node-start-skip-metadata)
+ (copy-marker)))
+ (end (thread-first sexp-node
+ (treesit-node-end)
+ (copy-marker))))
+ (when-let* ((parent (treesit-node-parent sexp-node))
+ ((not (string= (treesit-node-type parent) "source")))
+ (parent-beg (thread-first parent
+ (clojure-ts--node-start-skip-metadata)
+ (copy-marker)))
+ (parent-end (thread-first parent
+ (treesit-node-end)
+ (copy-marker))))
+ (save-excursion
+ (delete-region parent-beg beg)
+ (delete-region end parent-end)))))
+
+(defun clojure-ts--pop-out-of-threading ()
+ "Raise a sexp up a level to unwind a threading form."
+ (let* ((threading-sexp (clojure-ts--threading-sexp-node))
+ (beg (thread-first threading-sexp
+ (treesit-node-child 0 t)
+ (treesit-node-start))))
+ (save-excursion
+ (clojure-ts--skip-first-child threading-sexp)
+ (delete-region beg (point))
+ ;; `raise-sexp' doesn't work properly for function literals (it loses one
+ ;; of the parenthesis). Seems like an Emacs' bug.
+ (delete-pair))))
+
+(defun clojure-ts--fix-sexp-whitespace ()
+ "Fix whitespace after unwinding a threading form."
+ (save-excursion
+ (let ((beg (point)))
+ (treesit-end-of-thing 'sexp)
+ (indent-region beg (point))
+ (delete-trailing-whitespace beg (point)))))
+
+(defun clojure-ts--unwind-sexps-counter ()
+ "Return total number of s-expressions of a threading form at point."
+ (if-let* ((threading-sexp (clojure-ts--threading-sexp-node)))
+ (save-excursion
+ (clojure-ts--skip-first-child threading-sexp)
+ (let ((n 0))
+ (while (treesit-end-of-thing 'sexp 1 'restricted)
+ (setq n (1+ n)))
+ n))
+ (user-error "No threading form to unwind at point")))
+
+(defun clojure-ts-unwind (&optional n)
+ "Unwind thread at point or above point by N levels.
+
+With universal argument \\[universal-argument], fully unwinds thread."
+ (interactive "P")
+ (setq n (cond
+ ((equal n '(4)) (clojure-ts--unwind-sexps-counter))
+ (n)
+ (1)))
+ (if-let* ((threading-sexp (clojure-ts--threading-sexp-node))
+ (sym (clojure-ts--list-node-sym-text threading-sexp t)))
+ (save-excursion
+ (let ((beg (thread-first threading-sexp
+ (treesit-node-start)
+ (copy-marker)))
+ (end (thread-first threading-sexp
+ (treesit-node-end)
+ (copy-marker))))
+ (while (> n 0)
+ (cond
+ ((string-match-p (rx bol (* "some") "->" eol) sym)
+ (clojure-ts--unwind-thread-first))
+ ((string-match-p (rx bol (* "some") "->>" eol) sym)
+ (clojure-ts--unwind-thread-last)))
+ (setq n (1- n))
+ ;; After unwinding we check if it is the last expression and maybe
+ ;; splice it.
+ (when (clojure-ts--nothing-more-to-unwind)
+ (clojure-ts--pop-out-of-threading)
+ (clojure-ts--fix-sexp-whitespace)
+ (setq n 0)))
+ (indent-region beg end)
+ (delete-trailing-whitespace beg end)))
+ (user-error "No threading form to unwind at point")))
+
+(defun clojure-ts-unwind-all ()
+ "Fully unwind thread at point or above point."
+ (interactive)
+ (clojure-ts-unwind '(4)))
+
+(defun clojure-ts--remove-superfluous-parens ()
+ "Remove extra parens from a form."
+ (when-let* ((node (treesit-thing-at-point 'sexp 'nested))
+ ((clojure-ts--list-node-p node))
+ ((= 1 (treesit-node-child-count node t))))
+ (let ((delete-pair-blink-delay 0))
+ (delete-pair))))
+
+(defun clojure-ts--thread-first ()
+ "Thread a sexp using ->."
+ (save-excursion
+ (clojure-ts--skip-first-child (clojure-ts--threading-sexp-node))
+ (down-list)
+ (treesit-beginning-of-thing 'sexp -1)
+ (let ((contents (clojure-ts--delete-and-extract-sexp)))
+ (delete-char -1)
+ (when (looking-at-p " *\n")
+ (join-line 'following))
+ (backward-up-list)
+ (insert contents "\n")
+ (clojure-ts--remove-superfluous-parens))))
+
+(defun clojure-ts--thread-last ()
+ "Thread a sexp using ->>."
+ (save-excursion
+ (clojure-ts--skip-first-child (clojure-ts--threading-sexp-node))
+ (treesit-end-of-thing 'sexp)
+ (down-list -1)
+ (treesit-beginning-of-thing 'sexp)
+ (let ((contents (clojure-ts--delete-and-extract-sexp)))
+ (delete-char -1)
+ (treesit-end-of-thing 'sexp -1 'restricted)
+ (when (looking-at-p " *\n")
+ (join-line 'following))
+ (backward-up-list)
+ (insert contents "\n")
+ (clojure-ts--remove-superfluous-parens))))
+
+(defun clojure-ts--threadable-p (node)
+ "Return non-nil if expression NODE can be threaded.
+
+First argument after threading symbol itself should be a list and it
+should have more than one named child."
+ (let ((second-child (treesit-node-child node 1 t)))
+ (and (clojure-ts--list-node-p second-child)
+ (> (treesit-node-child-count second-child t) 1))))
+
+(defun clojure-ts-thread (&optional called-by-user-p)
+ "Thread by one more level an existing threading macro.
+
+If CALLED-BY-USER-P is non-nil (which is always TRUE when called
+interactively), the function signals a `user-error' if threading form
+cannot be found."
+ (interactive "p")
+ (if-let* ((threading-sexp (clojure-ts--threading-sexp-node))
+ ((clojure-ts--threadable-p threading-sexp))
+ (sym (clojure-ts--list-node-sym-text threading-sexp t)))
+ (let ((beg (thread-first threading-sexp
+ (treesit-node-start)
+ (copy-marker)))
+ (end (thread-first threading-sexp
+ (treesit-node-end)
+ (copy-marker))))
+ (cond
+ ((string-match-p (rx bol (* "some") "->" eol) sym)
+ (clojure-ts--thread-first))
+ ((string-match-p (rx bol (* "some") "->>" eol) sym)
+ (clojure-ts--thread-last)))
+ (indent-region beg end)
+ (delete-trailing-whitespace beg end)
+ t)
+ (when called-by-user-p
+ (user-error "No threading form at point"))))
+
+(defun clojure-ts--thread-all (first-or-last-thread but-last)
+ "Fully thread the form at point.
+
+FIRST-OR-LAST-THREAD is either \"->\" or \"->>\".
+
+When BUT-LAST is non-nil, the last expression is not threaded. Default
+value is `clojure-ts-thread-all-but-last.'"
+ (if-let* ((list-at-point (treesit-thing-at-point 'list 'nested)))
+ (save-excursion
+ (goto-char (treesit-node-start list-at-point))
+ (insert-parentheses 1)
+ (insert first-or-last-thread)
+ (while (clojure-ts-thread))
+ (when (or but-last clojure-ts-thread-all-but-last)
+ (clojure-ts-unwind)))
+ (user-error "No list to thread at point")))
+
+(defun clojure-ts-thread-first-all (but-last)
+ "Fully thread the form at point using ->.
+
+When BUT-LAST is non-nil, the last expression is not threaded. Default
+value is `clojure-ts-thread-all-but-last'."
+ (interactive "P")
+ (clojure-ts--thread-all "-> " but-last))
+
+(defun clojure-ts-thread-last-all (but-last)
+ "Fully thread the form at point using ->>.
+
+When BUT-LAST is non-nil, the last expression is not threaded. Default
+value is `clojure-ts-thread-all-but-last'."
+ (interactive "P")
+ (clojure-ts--thread-all "->> " but-last))
+
+(defun clojure-ts-cycle-privacy ()
+ "Make a definition at point public or private."
+ (interactive)
+ (if-let* ((node-at-point (treesit-node-at (point) 'clojure t))
+ (defun-node (treesit-parent-until node-at-point 'defun t)))
+ (save-excursion
+ (goto-char (treesit-node-start defun-node))
+ (search-forward-regexp (rx "def" (* letter) (? (group (or "-" " ^:private")))))
+ (if (match-string 1)
+ (replace-match "" nil nil nil 1)
+ (goto-char (match-end 0))
+ (insert (if (or clojure-ts-use-metadata-for-defn-privacy
+ (not (string= (match-string 0) "defn")))
+ " ^:private"
+ "-"))))
+ (user-error "No defun at point")))
+
+(defun clojure-ts--node-child (node predicate)
+ "Return the first child of the NODE that matches the PREDICATE.
+
+PREDICATE can be a symbol representing a thing in
+`treesit-thing-settings', or a predicate, like regexp matching node
+type, etc. See `treesit-thing-settings' for more details."
+ (thread-last (treesit-node-children node t)
+ (seq-find (lambda (child)
+ (treesit-node-match-p child predicate t)))))
+
+(defun clojure-ts--node-start-skip-metadata (node)
+ "Return NODE start position optionally skipping metadata."
+ (if (clojure-ts--metadata-node-p (treesit-node-child node 0 t))
+ (treesit-node-start (treesit-node-child node 1))
+ (treesit-node-start node)))
+
+(defun clojure-ts--add-arity-internal (fn-node)
+ "Add an arity to a function defined by FN-NODE."
+ (let* ((first-coll (clojure-ts--node-child fn-node (rx bol (or "vec_lit" "list_lit") eol)))
+ (coll-start (clojure-ts--node-start-skip-metadata first-coll))
+ (line-parent (thread-first fn-node
+ (clojure-ts--node-child-skip-metadata 0)
+ (treesit-node-start)
+ (line-number-at-pos)))
+ (line-args (line-number-at-pos coll-start))
+ (same-line-p (= line-parent line-args))
+ (single-arity-p (clojure-ts--vec-node-p first-coll)))
+ (goto-char coll-start)
+ (when same-line-p
+ (newline-and-indent))
+ (when single-arity-p
+ (insert-pair 2 ?\( ?\))
+ (backward-up-list))
+ (insert "([])\n")
+ ;; Put the point between square brackets.
+ (down-list -2)))
+
+(defun clojure-ts--add-arity-defprotocol-internal (fn-node)
+ "Add an arity to a defprotocol function defined by FN-NODE."
+ (let* ((args-vec (clojure-ts--node-child fn-node (rx bol "vec_lit" eol)))
+ (args-vec-start (clojure-ts--node-start-skip-metadata args-vec))
+ (line-parent (thread-first fn-node
+ (clojure-ts--node-child-skip-metadata 0)
+ (treesit-node-start)
+ (line-number-at-pos)))
+ (line-args-vec (line-number-at-pos args-vec-start))
+ (same-line-p (= line-parent line-args-vec)))
+ (goto-char args-vec-start)
+ (insert "[]")
+ (if same-line-p
+ (insert " ")
+ ;; If args vector is not at the same line, respect this and place each new
+ ;; vector on a new line.
+ (newline-and-indent))
+ ;; Put the point between square brackets.
+ (down-list -1)))
+
+(defun clojure-ts--add-arity-reify-internal (fn-node)
+ "Add an arity to a reify function defined by FN-NODE."
+ (let* ((fn-name (clojure-ts--list-node-sym-text fn-node)))
+ (goto-char (clojure-ts--node-start-skip-metadata fn-node))
+ (insert "(" fn-name " [])")
+ (newline-and-indent)
+ ;; Put the point between sqare brackets.
+ (down-list -2)))
+
+(defun clojure-ts--letfn-defn-p (node)
+ "Return non-nil if NODE is a function definition in a letfn form."
+ (when-let* ((parent (treesit-node-parent node)))
+ (and (clojure-ts--list-node-p node)
+ (clojure-ts--vec-node-p parent)
+ (let ((grandparent (treesit-node-parent parent)))
+ (string= (clojure-ts--list-node-sym-text grandparent)
+ "letfn")))))
+
+(defun clojure-ts--proxy-defn-p (node)
+ "Return non-nil if NODE is a function definition in a proxy form."
+ (when-let* ((parent (treesit-node-parent node)))
+ (and (clojure-ts--list-node-p node)
+ (string= (clojure-ts--list-node-sym-text parent) "proxy"))))
+
+(defun clojure-ts--defprotocol-defn-p (node)
+ "Return non-nil if NODE is a function definition in a defprotocol form."
+ (when-let* ((parent (treesit-node-parent node)))
+ (and (clojure-ts--list-node-p node)
+ (string= (clojure-ts--list-node-sym-text parent) "defprotocol"))))
+
+(defun clojure-ts--reify-defn-p (node)
+ "Return non-nil if NODE is a function definition in a reify form."
+ (when-let* ((parent (treesit-node-parent node)))
+ (and (clojure-ts--list-node-p node)
+ (string= (clojure-ts--list-node-sym-text parent) "reify"))))
+
+(defun clojure-ts-add-arity ()
+ "Add an arity to a function or macro."
+ (interactive)
+ (if-let* ((sym-regex (rx bol
+ (or "defn"
+ "letfn"
+ "fn"
+ "defmacro"
+ "defmethod"
+ "defprotocol"
+ "reify"
+ "proxy")
+ eol))
+ (parent-def-node (clojure-ts--search-list-form-at-point sym-regex))
+ (parent-def-sym (clojure-ts--list-node-sym-text parent-def-node))
+ (fn-node (cond
+ ((string= parent-def-sym "letfn")
+ (clojure-ts--parent-until #'clojure-ts--letfn-defn-p))
+ ((string= parent-def-sym "proxy")
+ (clojure-ts--parent-until #'clojure-ts--proxy-defn-p))
+ ((string= parent-def-sym "defprotocol")
+ (clojure-ts--parent-until #'clojure-ts--defprotocol-defn-p))
+ ((string= parent-def-sym "reify")
+ (clojure-ts--parent-until #'clojure-ts--reify-defn-p))
+ (t parent-def-node))))
+ (let ((beg-marker (copy-marker (treesit-node-start parent-def-node)))
+ (end-marker (copy-marker (treesit-node-end parent-def-node))))
+ (cond
+ ((string= parent-def-sym "defprotocol")
+ (clojure-ts--add-arity-defprotocol-internal fn-node))
+ ((string= parent-def-sym "reify")
+ (clojure-ts--add-arity-reify-internal fn-node))
+ (t (clojure-ts--add-arity-internal fn-node)))
+ (indent-region beg-marker end-marker))
+ (user-error "No suitable form to add an arity at point")))
+
+(defun clojure-ts-cycle-keyword-string ()
+ "Convert the string at point to a keyword, or vice versa."
+ (interactive)
+ (let ((node (treesit-thing-at-point 'sexp 'nested))
+ (pos (point)))
+ (cond
+ ((clojure-ts--string-node-p node)
+ (if (string-match-p " " (treesit-node-text node t))
+ (user-error "Cannot convert a string containing spaces to keyword")
+ (insert ?: (substring (clojure-ts--delete-and-extract-sexp) 1 -1))))
+ ((clojure-ts--keyword-node-p node)
+ (insert ?\" (substring (clojure-ts--delete-and-extract-sexp) 1) ?\"))
+ (t
+ (user-error "No string or keyword at point")))
+ (goto-char pos)))
+
+(defun clojure-ts--collection-node-at-point ()
+ "Return node at point that represent a collection."
+ (when-let* ((node (thread-first (point)
+ (treesit-node-at 'clojure)
+ (treesit-parent-until (rx bol
+ (or "map_lit"
+ "vec_lit"
+ "set_lit"
+ "list_lit"
+ "quoting_lit")
+ eol)))))
+ (cond
+ ;; If node is a list, check if it's quoted.
+ ((string= (treesit-node-type node) "list_lit")
+ (if-let* ((parent (treesit-node-parent node))
+ ((string= (treesit-node-type parent) "quoting_lit")))
+ parent
+ node))
+ ;; If the point is at the quote character, check if the child node is a
+ ;; list.
+ ((string= (treesit-node-type node) "quoting_lit")
+ (when-let* ((first-child (clojure-ts--node-child-skip-metadata node 0))
+ ((string= (treesit-node-type first-child) "list_lit")))
+ node))
+ (t node))))
+
+(defun clojure-ts--convert-collection (delim-open &optional prefix)
+ "Convert collection at point to another collection type.
+
+The original collection is being unwrapped and wrapped between
+DELIM-OPEN and its matching paren. If PREFIX is non-nil it's inserted
+before DELIM-OPEN."
+ (if-let* ((coll-node (clojure-ts--collection-node-at-point)))
+ (save-excursion
+ (goto-char (treesit-node-start coll-node))
+ (when (string-match-p (rx (or "set_lit" "quoting_lit"))
+ (treesit-node-type coll-node))
+ (delete-char 1))
+ (let ((parens-require-spaces nil)
+ (delete-pair-blink-delay 0))
+ (when prefix
+ (insert-char prefix))
+ (insert-pair 1 delim-open (matching-paren delim-open))
+ (delete-pair 1)))
+ (user-error "No collection at point to convert")))
+
+(defun clojure-ts-convert-collection-to-list ()
+ "Convert collection at point to list."
+ (interactive)
+ (clojure-ts--convert-collection ?\())
+
+(defun clojure-ts-convert-collection-to-quoted-list ()
+ "Convert collection at point to quoted list."
+ (interactive)
+ (clojure-ts--convert-collection ?\( ?'))
+
+(defun clojure-ts-convert-collection-to-map ()
+ "Convert collection at point to map."
+ (interactive)
+ (clojure-ts--convert-collection ?{))
+
+(defun clojure-ts-convert-collection-to-vector ()
+ "Convert collection at point to vector."
+ (interactive)
+ (clojure-ts--convert-collection ?\[))
+
+(defun clojure-ts-convert-collection-to-set ()
+ "Convert collection at point to set."
+ (interactive)
+ (clojure-ts--convert-collection ?{ ?#))
+
+(defun clojure-ts-cycle-conditional ()
+ "Change a surrounding conditional form to its negated counterpart, or vice versa."
+ (interactive)
+ (if-let* ((sym-regex (rx bol
+ (or "if" "if-not" "when" "when-not")
+ eol))
+ (cond-node (clojure-ts--search-list-form-at-point sym-regex t))
+ (cond-sym (clojure-ts--list-node-sym-text cond-node)))
+ (let ((beg (treesit-node-start cond-node))
+ (end-marker (copy-marker (treesit-node-end cond-node)))
+ (new-sym (pcase cond-sym
+ ("if" "if-not")
+ ("if-not" "if")
+ ("when" "when-not")
+ ("when-not" "when"))))
+ (save-excursion
+ (goto-char (clojure-ts--node-start-skip-metadata cond-node))
+ (down-list 1)
+ (delete-char (length cond-sym))
+ (insert new-sym)
+ (when (member cond-sym '("if" "if-not"))
+ (forward-sexp 2)
+ (transpose-sexps 1))
+ (indent-region beg end-marker)))
+ (user-error "No conditional expression found")))
+
+(defun clojure-ts--point-outside-node-p (node)
+ "Return non-nil if point is outside of the actual NODE start.
+
+Clojure grammar treats metadata as part of an expression, so for example
+^boolean (not (= 2 2)) is a single list node, including metadata. This
+causes issues for functions that navigate by s-expressions and lists.
+This function returns non-nil if point is outside of the outermost
+parenthesis."
+ (let* ((actual-node-start (clojure-ts--node-start-skip-metadata node))
+ (node-end (treesit-node-end node))
+ (pos (point)))
+ (or (< pos actual-node-start)
+ (> pos node-end))))
+
+(defun clojure-ts-cycle-not ()
+ "Add or remove a not form around the current form."
+ (interactive)
+ (if-let* ((list-node (clojure-ts--parent-until (rx bol "list_lit" eol)))
+ ((not (clojure-ts--point-outside-node-p list-node))))
+ (let ((beg (treesit-node-start list-node))
+ (end-marker (copy-marker (treesit-node-end list-node)))
+ (pos (copy-marker (point) t)))
+ (goto-char (clojure-ts--node-start-skip-metadata list-node))
+ (if-let* ((list-parent (treesit-node-parent list-node))
+ ((clojure-ts--list-node-sym-match-p list-parent (rx bol "not" eol))))
+ (clojure-ts--raise-sexp)
+ (insert-pair 1 ?\( ?\))
+ (insert "not "))
+ (indent-region beg end-marker)
+ ;; `save-excursion' doesn't work well when point is at the opening
+ ;; paren.
+ (goto-char pos))
+ (user-error "Must be invoked inside a list")))
+
+(defvar clojure-ts-refactor-map
+ (let ((map (make-sparse-keymap)))
+ (keymap-set map "C-t" #'clojure-ts-thread)
+ (keymap-set map "t" #'clojure-ts-thread)
+ (keymap-set map "C-u" #'clojure-ts-unwind)
+ (keymap-set map "u" #'clojure-ts-unwind)
+ (keymap-set map "C-f" #'clojure-ts-thread-first-all)
+ (keymap-set map "f" #'clojure-ts-thread-first-all)
+ (keymap-set map "C-l" #'clojure-ts-thread-last-all)
+ (keymap-set map "l" #'clojure-ts-thread-last-all)
+ (keymap-set map "C-p" #'clojure-ts-cycle-privacy)
+ (keymap-set map "p" #'clojure-ts-cycle-privacy)
+ (keymap-set map "C-(" #'clojure-ts-convert-collection-to-list)
+ (keymap-set map "(" #'clojure-ts-convert-collection-to-list)
+ (keymap-set map "C-'" #'clojure-ts-convert-collection-to-quoted-list)
+ (keymap-set map "'" #'clojure-ts-convert-collection-to-quoted-list)
+ (keymap-set map "C-{" #'clojure-ts-convert-collection-to-map)
+ (keymap-set map "{" #'clojure-ts-convert-collection-to-map)
+ (keymap-set map "C-[" #'clojure-ts-convert-collection-to-vector)
+ (keymap-set map "[" #'clojure-ts-convert-collection-to-vector)
+ (keymap-set map "C-#" #'clojure-ts-convert-collection-to-set)
+ (keymap-set map "#" #'clojure-ts-convert-collection-to-set)
+ (keymap-set map "C-c" #'clojure-ts-cycle-conditional)
+ (keymap-set map "c" #'clojure-ts-cycle-conditional)
+ (keymap-set map "C-o" #'clojure-ts-cycle-not)
+ (keymap-set map "o" #'clojure-ts-cycle-not)
+ (keymap-set map "C-a" #'clojure-ts-add-arity)
+ (keymap-set map "a" #'clojure-ts-add-arity)
+ map)
+ "Keymap for `clojure-ts-mode' refactoring commands.")
+
(defvar clojure-ts-mode-map
(let ((map (make-sparse-keymap)))
;;(set-keymap-parent map clojure-mode-map)
- map))
+ (keymap-set map "C-:" #'clojure-ts-cycle-keyword-string)
+ (keymap-set map "C-c SPC" #'clojure-ts-align)
+ (keymap-set map clojure-ts-refactor-map-prefix clojure-ts-refactor-map)
+ (easy-menu-define clojure-ts-mode-menu map "Clojure[TS] Mode Menu"
+ '("Clojure"
+ ["Toggle between string & keyword" clojure-ts-cycle-keyword-string]
+ ["Align expression" clojure-ts-align]
+ ["Cycle privacy" clojure-ts-cycle-privacy]
+ ["Cycle conditional" clojure-ts-cycle-conditional]
+ ["Cycle not" clojure-ts-cycle-not]
+ ["Add function/macro arity" clojure-ts-add-arity]
+ ("Convert collection"
+ ["Convert to list" clojure-ts-convert-collection-to-list]
+ ["Convert to quoted list" clojure-ts-convert-collection-to-quoted-list]
+ ["Convert to map" clojure-ts-convert-collection-to-map]
+ ["Convert to vector" clojure-ts-convert-collection-to-vector]
+ ["Convert to set" clojure-ts-convert-collection-to-set])
+ ("Refactor -> and ->>"
+ ["Thread once more" clojure-ts-thread]
+ ["Fully thread a form with ->" clojure-ts-thread-first-all]
+ ["Fully thread a form with ->>" clojure-ts-thread-last-all]
+ "--"
+ ["Unwind once" clojure-ts-unwind]
+ ["Fully unwind a threading macro" clojure-ts-unwind-all])))
+ map)
+ "Keymap for `clojure-ts-mode'.")
(defvar clojure-ts-clojurescript-mode-map
(let ((map (make-sparse-keymap)))
@@ -1315,7 +2463,9 @@ If JUSTIFY is non-nil, justify as well as fill the paragraph."
"v0.0.13")
(markdown-inline "https://github.com/MDeiml/tree-sitter-markdown"
"v0.4.1"
- "tree-sitter-markdown-inline/src"))
+ "tree-sitter-markdown-inline/src")
+ (regex "https://github.com/tree-sitter/tree-sitter-regex"
+ "v0.24.3"))
"Intended to be used as the value for `treesit-language-source-alist'.")
(defun clojure-ts--ensure-grammars ()
@@ -1324,7 +2474,7 @@ If JUSTIFY is non-nil, justify as well as fill the paragraph."
(dolist (recipe clojure-ts-grammar-recipes)
(let ((grammar (car recipe)))
(unless (treesit-language-available-p grammar nil)
- (message "Installing %s tree-sitter grammar" grammar)
+ (message "Installing %s Tree-sitter grammar" grammar)
;; `treesit-language-source-alist' is dynamically scoped.
;; Binding it in this let expression allows
;; `treesit-install-language-gramamr' to pick up the grammar recipes
@@ -1340,23 +2490,33 @@ function can also be used to upgrade the grammars if they are outdated."
(interactive)
(dolist (recipe clojure-ts-grammar-recipes)
(let ((grammar (car recipe)))
- (message "Installing %s tree-sitter grammar" grammar)
+ (message "Installing %s Tree-sitter grammar" grammar)
(let ((treesit-language-source-alist clojure-ts-grammar-recipes))
(treesit-install-language-grammar grammar)))))
-(defun clojure-ts-mode-variables (&optional markdown-available)
+(defun clojure-ts-mode-variables (&optional markdown-available regex-available)
"Initialize buffer-local variables for `clojure-ts-mode'.
-See `clojure-ts--font-lock-settings' for usage of MARKDOWN-AVAILABLE."
+
+See `clojure-ts--font-lock-settings' for usage of MARKDOWN-AVAILABLE and
+REGEX-AVAILABLE."
+ (setq-local indent-tabs-mode nil)
(setq-local comment-add 1)
(setq-local comment-start ";")
+ (when (equal clojure-ts-outline-variant 'comments)
+ ;; NOTE: If `imenu' option is selected for `clojure-ts-outline-variant', all
+ ;; necessary variables will be set automatically by
+ ;; `treesit-major-mode-setup'.
+ (setq-local treesit-outline-predicate #'clojure-ts--outline-predicate
+ outline-search-function #'treesit-outline-search
+ outline-level #'clojure-ts--outline-level))
(setq-local treesit-font-lock-settings
- (clojure-ts--font-lock-settings markdown-available))
+ (clojure-ts--font-lock-settings markdown-available regex-available))
(setq-local treesit-font-lock-feature-list
'((comment definition variable)
(keyword string char symbol builtin type)
- (constant number quote metadata doc)
- (bracket deref function regex tagged-literals)))
+ (constant number quote metadata doc regex)
+ (bracket deref function tagged-literals)))
(setq-local treesit-defun-prefer-top-level t)
(setq-local treesit-defun-tactic 'top-level)
@@ -1389,13 +2549,16 @@ See `clojure-ts--font-lock-settings' for usage of MARKDOWN-AVAILABLE."
:syntax-table clojure-ts-mode-syntax-table
(clojure-ts--ensure-grammars)
(let ((use-markdown-inline (and clojure-ts-use-markdown-inline
- (treesit-ready-p 'markdown-inline t))))
- (when use-markdown-inline
- (setq-local treesit-range-settings clojure-ts--treesit-range-settings))
+ (treesit-ready-p 'markdown-inline t)))
+ (use-regex (and clojure-ts-use-regex-parser
+ (treesit-ready-p 'regex t))))
+ (setq-local treesit-range-settings
+ (clojure-ts--treesit-range-settings use-markdown-inline
+ use-regex))
(when (treesit-ready-p 'clojure)
(treesit-parser-create 'clojure)
- (clojure-ts-mode-variables use-markdown-inline)
+ (clojure-ts-mode-variables use-markdown-inline use-regex)
(when clojure-ts--debug
(setq-local treesit--indent-verbose t)
@@ -1405,6 +2568,11 @@ See `clojure-ts--font-lock-settings' for usage of MARKDOWN-AVAILABLE."
(treesit-major-mode-setup)
+ ;; We should assign this after calling `treesit-major-mode-setup',
+ ;; otherwise it will be owerwritten.
+ (when clojure-ts-align-forms-automatically
+ (setq-local indent-region-function #'clojure-ts-indent-region))
+
;; Initial indentation rules cache calculation.
(setq clojure-ts--semantic-indent-rules-cache
(clojure-ts--compute-semantic-indentation-rules-cache clojure-ts-semantic-indent-rules))
@@ -1497,7 +2665,7 @@ Useful if you want to switch to the `clojure-mode's mode mappings."
;; nbb scripts are ClojureScript source files
(add-to-list 'interpreter-mode-alist '("nbb" . clojure-ts-clojurescript-mode))
(clojure-ts--register-novel-modes)))
- (message "Clojure TS Mode will not be activated as tree-sitter support is missing."))
+ (message "Clojure TS Mode will not be activated as Tree-sitter support is missing."))
(defvar clojure-ts--find-ns-query
(treesit-query-compile
diff --git a/doc/design.md b/doc/design.md
index 0d2df9c..8afeaff 100644
--- a/doc/design.md
+++ b/doc/design.md
@@ -4,47 +4,50 @@ This document is still a work in progress.
Clojure-ts-mode is based on the tree-sitter-clojure grammar.
-If you want to contribute to clojure-ts-mode, it is recommend that you familiarize yourself with how tree-sitter works.
-The official documentation is a great place to start: https://tree-sitter.github.io/tree-sitter/
-These guides for Emacs tree-sitter development are also useful
-- https://casouri.github.io/note/2023/tree-sitter-starter-guide/index.html
+If you want to contribute to clojure-ts-mode, it is recommend that you familiarize yourself with how Tree-sitter works.
+The official documentation is a great place to start:
+These guides for Emacs Tree-sitter development are also useful
+
+-
- `Developing major modes with tree-sitter` (From the Emacs 29+ Manual, `C-h i`, search for `tree-sitter`)
In short:
-Tree-sitter is a tool that generates parser libraries for programming languages, and provides an API for interacting with those parsers.
-The generated parsers can create syntax trees from source code text.
-The nodes of those trees are defined by the grammar.
-Emacs can use these generated parsers to provide major modes with things like syntax highlighting, indentation, navigation, structural editing, and many other things.
+
+- Tree-sitter is a tool that generates parser libraries for programming languages, and provides an API for interacting with those parsers.
+- The generated parsers can create syntax trees from source code text.
+- The nodes of those trees are defined by the grammar.
+- Emacs can use these generated parsers to provide major modes with things like syntax highlighting, indentation, navigation, structural editing, and many other things.
## Important Definitions
-- Parser: A dynamic library compiled from C source code that is generated by the tree-sitter tool. A parser reads source code for a particular language and produces a syntax tree.
-- Grammar: The rules that define how a parser will create the syntax tree for a language. The grammar is written in javascript. Tree-sitter tooling consumes the grammar as input and outputs C source (which can be compiled into a parser)
+- Parser: A dynamic library compiled from C source code that is generated by the Tree-sitter tool. A parser reads source code for a particular language and produces a syntax tree.
+- Grammar: The rules that define how a parser will create the syntax tree for a language. The grammar is written in JavaScript. Tree-sitter tooling consumes the grammar as input and outputs C source (which can be compiled into a parser)
- Syntax Tree: a tree data structure comprised of syntax nodes that represents some source code text.
- - Concrete Syntax Tree: Syntax trees that contain nodes for every token in the source code, including things likes brackets and parentheses. Tree-sitter creates Concrete Syntax Trees.
- - Abstract Syntax Tree: A syntax tree with less important details removed. An AST may contain a node for a list, but not individual parentheses. Tree-sitter does not create Abstract Syntax Trees.
+ - Concrete Syntax Tree: Syntax trees that contain nodes for every token in the source code, including things likes brackets and parentheses. Tree-sitter creates Concrete Syntax Trees.
+ - Abstract Syntax Tree: A syntax tree with less important details removed. An AST may contain a node for a list, but not individual parentheses. Tree-sitter does not create Abstract Syntax Trees.
- Syntax Node: A node in a syntax tree. It represents some subset of a source code text. Each node has a type, defined by the grammar used to produce it. Some common node types represent language constructs like strings, integers, operators.
- - Named Syntax Node: A node that can be identified by a name given to it in the tree-sitter Grammar. In clojure-ts-mode, `list_lit` is a named node for lists.
- - Anonymous Syntax Node: A node that cannot be identified by a name. In the Grammar these are identified by simple strings, not by complex Grammar rules. In clojure-ts-mode, `"("` and `")"` are anonymous nodes.
+ - Named Syntax Node: A node that can be identified by a name given to it in the Tree-sitter Grammar. In clojure-ts-mode, `list_lit` is a named node for lists.
+ - Anonymous Syntax Node: A node that cannot be identified by a name. In the Grammar these are identified by simple strings, not by complex Grammar rules. In clojure-ts-mode, `"("` and `")"` are anonymous nodes.
- Font Locking: What Emacs calls "Syntax Highlighting".
## tree-sitter-clojure
-Clojure-ts-mode uses the tree-sitter-clojure grammar, which can be found at https://github.com/sogaiu/tree-sitter-clojure
-The clojure-ts-mode grammar provides very basic, low level nodes that try to match clojure's very light syntax.
+Clojure-ts-mode uses the tree-sitter-clojure grammar, which can be found at
+The clojure-ts-mode grammar provides very basic, low level nodes that try to match Clojure's very light syntax.
There are nodes to represent:
+
- Symbols (sym_lit)
- - Contain (sym_ns) and (sym_name) nodes
+ - Contain (sym_ns) and (sym_name) nodes
- Keywords (kwd_lit)
- - Contain (kwd_ns) and (kw_name) nodes
+ - Contain (kwd_ns) and (kw_name) nodes
- Strings (str_lit)
- Chars (char_lit)
- Nil (nil_lit)
- Booleans (bool_lit)
- Numbers (num_lit)
- Comments (comment, dis_expr)
- - dis_expr is the `#_` discard expression
+ - dis_expr is the `#_` discard expression
- Lists (list_list)
- Vectors (vec_lit)
- Maps (map_lit)
@@ -61,7 +64,7 @@ will produce a parse tree like so
```
(vec_lit
- meta: (meta_lit
+ meta: (meta_lit
value: (kwd_lit name: (kwd_name)))
value: (num_lit))
```
@@ -70,12 +73,12 @@ The best place to learn more about the tree-sitter-clojure grammar is to read th
### Clojure Syntax, not Clojure Semantics
-An important observation that anyone familiar with popular tree-sitter grammars may have picked up on is that there are no nodes representing things like functions, macros, types, and other semantic concepts.
-Representing the semantics of Clojure in a tree-sitter grammar is much more difficult than traditional languages that do not use macros heavily like Clojure and other lisps.
-To understand what an expression represents in Clojure source code requires macro-expansion of the source code.
-Macro-expansion requires a runtime, and tree-sitter does not have access to a Clojure runtime and will never have access to a Clojure runtime.
-Additionally tree-sitter never looks back on what it has parsed, only forward, considering what is directly ahead of it. So even if it could identify a macro like `myspecialdef` it would forget about it as soon as it moved passed the declaring `defmacro` node.
-Another way to think about this: tree-sitter is designed to be fast and good-enough for tooling to implement syntax highlighting, indentation, and other editing conveniences. It is not meant for interpreting and execution.
+An important observation that anyone familiar with popular Tree-sitter grammars may have picked up on is that there are no nodes representing things like functions, macros, types, and other semantic concepts.
+Representing the semantics of Clojure in a Tree-sitter grammar is much more difficult than traditional languages that do not use macros heavily like Clojure and other lisps.
+To understand what an expression represents in Clojure source code requires macro-expansion of the source code.
+Macro-expansion requires a runtime, and Tree-sitter does not have access to a Clojure runtime and will never have access to a Clojure runtime.
+Additionally Tree-sitter never looks back on what it has parsed, only forward, considering what is directly ahead of it. So even if it could identify a macro like `myspecialdef` it would forget about it as soon as it moved passed the declaring `defmacro` node.
+Another way to think about this: Tree-sitter is designed to be fast and good-enough for tooling to implement syntax highlighting, indentation, and other editing conveniences. It is not meant for interpreting and execution.
#### Example 1: False Negative Function Classification
@@ -88,9 +91,8 @@ Consider the following macro
(defn2 dog [] "bark")
```
-
This macro lets the caller define a function, but a hypothetical tree-sitter-clojure semantic grammar might just see a function call where a variable dog is passed as an argument.
-How should tree-sitter know that `dog` should be highlighted like function? It would have to evaluate the `defn2` macro to understand that.
+How should Tree-sitter know that `dog` should be highlighted like function? It would have to evaluate the `defn2` macro to understand that.
#### Example 2: False Positive Function Classification
@@ -105,13 +107,13 @@ How should tree-sitter know that `dog` should be highlighted like function? It w
evaluates to 1, and the following
-```
+```clojure
(foo)
```
evaluates to 1.
-How is tree-sitter supposed to understand that `(defn foo [] 2)` of the expression `(no-defn (defn foo [] 2))` is not a function declaration? It would have to evaluate the `no-defn` macro.
+How is Tree-sitter supposed to understand that `(defn foo [] 2)` of the expression `(no-defn (defn foo [] 2))` is not a function declaration? It would have to evaluate the `no-defn` macro.
#### Syntax and Semantics: Conclusions
@@ -122,17 +124,27 @@ Instead, it is up to the emacs-lisp code and other consumers of the tree-sitter-
There are some pros and cons of this decision for tree-sitter-clojure to only consider syntax and not semantics.
Some of the (non-exhaustive) upsides:
+
- No semantic false positives or negatives in the parse tree.
- Simple grammar to maintain with less nodes and rules
- Small, fast grammar (with a small set of grammar rules, tree-sitter-clojure has one of the smallest binaries and fastest grammars in widespread use)
- Stability: the grammar changes infrequently and is very stable for downstream consumers
-And the primary downside: Semantics must be (re)-implemented in tools that consume the grammar. While this results in more work for tooling authors, the tools that use the grammar are easier to change than the grammar itself. The inaccurate nature of statically interpreting Clojure semantics means that not every decision made for the grammar would meet the needs of the various grammar consumers. This would lead to bugs and feature requests. Nearly all changes to the grammar will result in some sort of breakages to its consumers, so changes are best avoided once the grammar has stabilized. Therefore avoiding these semantic interpretations in the grammar is one of the best ways to minimize changes in the grammar.
+And the primary downside: Semantics must be (re)-implemented in tools that
+consume the grammar. While this results in more work for tooling authors, the
+tools that use the grammar are easier to change than the grammar itself. The
+inaccurate nature of statically interpreting Clojure semantics means that not
+every decision made for the grammar would meet the needs of the various grammar
+consumers. This would lead to bugs and feature requests. Nearly all changes to
+the grammar will result in some sort of breakages to its consumers, so changes
+are best avoided once the grammar has stabilized. Therefore avoiding these
+semantic interpretations in the grammar is one of the best ways to minimize
+changes in the grammar.
#### Further Reading
-- https://github.com/sogaiu/tree-sitter-clojure/blob/master/doc/scope.md
-- https://tree-sitter.github.io/tree-sitter/using-parsers#named-vs-anonymous-nodes
+-
+-
## Syntax Highlighting
diff --git a/screenshots/markdown-syntax-dark-theme.png b/screenshots/markdown-syntax-dark-theme.png
new file mode 100644
index 0000000..7a908ac
Binary files /dev/null and b/screenshots/markdown-syntax-dark-theme.png differ
diff --git a/screenshots/regex-syntax-dark-theme.png b/screenshots/regex-syntax-dark-theme.png
new file mode 100644
index 0000000..ad7ee45
Binary files /dev/null and b/screenshots/regex-syntax-dark-theme.png differ
diff --git a/test/clojure-ts-mode-convert-collection-test.el b/test/clojure-ts-mode-convert-collection-test.el
new file mode 100644
index 0000000..05e04f6
--- /dev/null
+++ b/test/clojure-ts-mode-convert-collection-test.el
@@ -0,0 +1,119 @@
+;;; clojure-ts-mode-convert-collection-test.el --- Clojure[TS] Mode convert collection type. -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Roman Rudakov
+
+;; Author: Roman Rudakov
+
+;; 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 .
+
+;;; Commentary:
+
+;; Adapted from `clojure-mode'.
+
+;;; Code:
+
+(require 'clojure-ts-mode)
+(require 'buttercup)
+(require 'test-helper "test/test-helper")
+
+(describe "clojure-ts-convert-collection-to-map"
+ (when-refactoring-it "should convert a list to a map"
+ "(:a 1 :b 2)"
+ "{:a 1 :b 2}"
+ (backward-sexp)
+ (down-list)
+ (clojure-ts-convert-collection-to-map))
+
+ (it "should signal a user error when there is no collection at point"
+ (with-clojure-ts-buffer "false"
+ (backward-sexp)
+ (expect (clojure-ts-convert-collection-to-map)
+ :to-throw
+ 'user-error
+ '("No collection at point to convert")))))
+
+(describe "clojure-ts-convert-collection-to-vector"
+ (when-refactoring-it "should convert a map to a vector"
+ "{:a 1 :b 2}"
+ "[:a 1 :b 2]"
+ (backward-sexp)
+ (down-list)
+ (clojure-ts-convert-collection-to-vector))
+
+ (it "should signal a user error when there is no collection at point"
+ (with-clojure-ts-buffer "false"
+ (backward-sexp)
+ (expect (clojure-ts-convert-collection-to-vector)
+ :to-throw
+ 'user-error
+ '("No collection at point to convert")))))
+
+(describe "clojure-ts-convert-collection-to-set"
+ (when-refactoring-it "should convert a vector to a set"
+ "[1 2 3]"
+ "#{1 2 3}"
+ (backward-sexp)
+ (down-list)
+ (clojure-ts-convert-collection-to-set))
+
+ (when-refactoring-it "should convert a quoted list to a set"
+ "'(1 2 3)"
+ "#{1 2 3}"
+ (backward-sexp)
+ (down-list)
+ (clojure-ts-convert-collection-to-set))
+
+ (it "should signal a user error when there is no collection at point"
+ (with-clojure-ts-buffer "false"
+ (backward-sexp)
+ (expect (clojure-ts-convert-collection-to-set)
+ :to-throw
+ 'user-error
+ '("No collection at point to convert")))))
+
+(describe "clojure-ts-convert-collection-to-list"
+ (when-refactoring-it "should convert a set to a list"
+ "#{1 2 3}"
+ "(1 2 3)"
+ (backward-sexp)
+ (down-list)
+ (clojure-ts-convert-collection-to-list))
+
+ (it "should signal a user error when there is no collection at point"
+ (with-clojure-ts-buffer "false"
+ (backward-sexp)
+ (expect (clojure-ts-convert-collection-to-list)
+ :to-throw
+ 'user-error
+ '("No collection at point to convert")))))
+
+(describe "clojure-ts-convert-collection-to-quoted-list"
+ (when-refactoring-it "should convert a set to a quoted list"
+ "#{1 2 3}"
+ "'(1 2 3)"
+ (backward-sexp)
+ (down-list)
+ (clojure-ts-convert-collection-to-quoted-list))
+
+ (it "should signal a user error when there is no collection at point"
+ (with-clojure-ts-buffer "false"
+ (backward-sexp)
+ (expect (clojure-ts-convert-collection-to-quoted-list)
+ :to-throw
+ 'user-error
+ '("No collection at point to convert")))))
+
+
+(provide 'clojure-ts-mode-convert-collection-test)
+;;; clojure-ts-mode-convert-collection-test.el ends here
diff --git a/test/clojure-ts-mode-cycling-test.el b/test/clojure-ts-mode-cycling-test.el
new file mode 100644
index 0000000..81eef67
--- /dev/null
+++ b/test/clojure-ts-mode-cycling-test.el
@@ -0,0 +1,272 @@
+;;; clojure-ts-mode-cycling-test.el --- Clojure[TS] Mode: cycling things tests -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Roman Rudakov
+
+;; Author: Roman Rudakov
+
+;; 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 .
+
+;;; Commentary:
+
+;; The code is adapted from `clojure-mode'.
+
+;;; Code:
+
+(require 'clojure-ts-mode)
+(require 'buttercup)
+(require 'test-helper "test/test-helper")
+
+(describe "clojure-ts-cycle-keyword-string"
+ (when-refactoring-with-point-it "should convert string to keyword"
+ "\"hel|lo\""
+
+ ":hel|lo"
+
+ (clojure-ts-cycle-keyword-string))
+
+ (when-refactoring-with-point-it "should convert keyword to string"
+ ":|hello"
+
+ "\"|hello\""
+
+ (clojure-ts-cycle-keyword-string))
+
+ (it "should signal a user error when there is nothing to convert at point"
+ (with-clojure-ts-buffer "[true false]"
+ (goto-char 2)
+ (expect (clojure-ts-cycle-keyword-string)
+ :to-throw
+ 'user-error
+ '("No string or keyword at point"))))
+
+ (it "should signal a user error when string at point contains spaces"
+ (with-clojure-ts-buffer "\"Hello world\""
+ (goto-char 2)
+ (expect (clojure-ts-cycle-keyword-string)
+ :to-throw
+ 'user-error
+ '("Cannot convert a string containing spaces to keyword")))))
+
+(describe "clojure-ts-cycle-privacy"
+
+ (when-refactoring-it "should turn a public defn into a private defn"
+ "(defn add [a b]
+ (+ a b))"
+
+ "(defn- add [a b]
+ (+ a b))"
+
+ (clojure-ts-cycle-privacy))
+
+ (when-refactoring-it "should also work from the beginning of a sexp"
+ "(defn- add [a b]
+ (+ a b))"
+
+ "(defn add [a b]
+ (+ a b))"
+
+ (backward-sexp)
+ (clojure-ts-cycle-privacy))
+
+ (when-refactoring-it "should use metadata when clojure-use-metadata-for-privacy is set to true"
+ "(defn add [a b]
+ (+ a b))"
+
+ "(defn ^:private add [a b]
+ (+ a b))"
+
+ (let ((clojure-ts-use-metadata-for-defn-privacy t))
+ (clojure-ts-cycle-privacy)))
+
+ (when-refactoring-it "should turn a private defn into a public defn"
+ "(defn- add [a b]
+ (+ a b))"
+
+ "(defn add [a b]
+ (+ a b))"
+
+ (clojure-ts-cycle-privacy))
+
+ (when-refactoring-it "should turn a private defn with metadata into a public defn"
+ "(defn ^:private add [a b]
+ (+ a b))"
+
+ "(defn add [a b]
+ (+ a b))"
+
+ (let ((clojure-ts-use-metadata-for-defn-privacy t))
+ (clojure-ts-cycle-privacy)))
+
+ (when-refactoring-it "should also work with pre-existing metadata"
+ "(def ^:dynamic config
+ \"docs\"
+ {:env \"staging\"})"
+
+ "(def ^:private ^:dynamic config
+ \"docs\"
+ {:env \"staging\"})"
+
+ (clojure-ts-cycle-privacy))
+
+ (when-refactoring-it "should turn a private def with metadata into a public def"
+ "(def ^:private config
+ \"docs\"
+ {:env \"staging\"})"
+
+ "(def config
+ \"docs\"
+ {:env \"staging\"})"
+
+ (clojure-ts-cycle-privacy))
+
+ (when-refactoring-it "should turn a public defmulti into a private defmulti"
+ "(defmulti service-charge (juxt account-level :tag))"
+
+ "(defmulti ^:private service-charge (juxt account-level :tag))"
+
+ (clojure-ts-cycle-privacy))
+
+ (when-refactoring-it "should turn a private defmulti into a public defmulti"
+ "(defmulti ^:private service-charge (juxt account-level :tag))"
+
+ "(defmulti service-charge (juxt account-level :tag))"
+
+ (clojure-ts-cycle-privacy))
+
+ (when-refactoring-it "should turn a public defmacro into a private defmacro"
+ "(defmacro unless [pred a b]
+ `(if (not ~pred) ~a ~b))"
+
+ "(defmacro ^:private unless [pred a b]
+ `(if (not ~pred) ~a ~b))"
+
+ (clojure-ts-cycle-privacy))
+
+ (when-refactoring-it "should turn a private defmacro into a public defmacro"
+ "(defmacro ^:private unless [pred a b]
+ `(if (not ~pred) ~a ~b))"
+
+ "(defmacro unless [pred a b]
+ `(if (not ~pred) ~a ~b))"
+
+ (clojure-ts-cycle-privacy))
+
+ (when-refactoring-it "should turn a private definline into a public definline"
+ "(definline bad-sqr [x] `(* ~x ~x))"
+
+ "(definline ^:private bad-sqr [x] `(* ~x ~x))"
+
+ (clojure-ts-cycle-privacy))
+
+ (when-refactoring-it "should turn a public definline into a private definline"
+ "(definline ^:private bad-sqr [x] `(* ~x ~x))"
+
+ "(definline bad-sqr [x] `(* ~x ~x))"
+
+ (clojure-ts-cycle-privacy))
+
+ (when-refactoring-it "should turn a private defrecord into a public defrecord"
+ "(defrecord Person [fname lname address])"
+
+ "(defrecord ^:private Person [fname lname address])"
+
+ (clojure-ts-cycle-privacy))
+
+ (when-refactoring-it "should turn a public defrecord into a private defrecord"
+ "(defrecord ^:private Person [fname lname address])"
+
+ "(defrecord Person [fname lname address])"
+
+ (clojure-ts-cycle-privacy)))
+
+(describe "clojure-cycle-if"
+
+ (when-refactoring-with-point-it "should cycle inner if"
+ "(if this
+ (if |that
+ (then AAA)
+ (else BBB))
+ (otherwise CCC))"
+
+ "(if this
+ (if-not |that
+ (else BBB)
+ (then AAA))
+ (otherwise CCC))"
+
+ (clojure-ts-cycle-conditional))
+
+ (when-refactoring-with-point-it "should cycle outer if"
+ "(if-not |this
+ (if that
+ (then AAA)
+ (else BBB))
+ (otherwise CCC))"
+
+ "(if |this
+ (otherwise CCC)
+ (if that
+ (then AAA)
+ (else BBB)))"
+
+ (clojure-ts-cycle-conditional)))
+
+(describe "clojure-cycle-when"
+
+ (when-refactoring-with-point-it "should cycle inner when"
+ "(when this
+ (when |that
+ (aaa)
+ (bbb))
+ (ccc))"
+
+ "(when this
+ (when-not |that
+ (aaa)
+ (bbb))
+ (ccc))"
+
+ (clojure-ts-cycle-conditional))
+
+ (when-refactoring-with-point-it "should cycle outer when"
+ "(when-not |this
+ (when that
+ (aaa)
+ (bbb))
+ (ccc))"
+
+ "(when |this
+ (when that
+ (aaa)
+ (bbb))
+ (ccc))"
+
+ (clojure-ts-cycle-conditional)))
+
+(describe "clojure-cycle-not"
+
+ (when-refactoring-with-point-it "should add a not when missing"
+ "(ala bala| portokala)"
+ "(not (ala bala| portokala))"
+
+ (clojure-ts-cycle-not))
+
+ (when-refactoring-with-point-it "should remove a not when present"
+ "(not (ala bala| portokala))"
+ "(ala bala| portokala)"
+
+ (clojure-ts-cycle-not)))
+
+(provide 'clojure-ts-mode-cycling-test)
+;;; clojure-ts-mode-cycling-test.el ends here
diff --git a/test/clojure-ts-mode-font-lock-test.el b/test/clojure-ts-mode-font-lock-test.el
index 02e0fa4..8611211 100644
--- a/test/clojure-ts-mode-font-lock-test.el
+++ b/test/clojure-ts-mode-font-lock-test.el
@@ -34,9 +34,9 @@
(declare (debug t)
(indent 1))
`(with-clojure-ts-buffer ,content
- (font-lock-ensure)
- (goto-char (point-min))
- ,@body))
+ (font-lock-ensure)
+ (goto-char (point-min))
+ ,@body))
(defun clojure-ts-get-face-at (start end content)
"Get the face between START and END in CONTENT."
@@ -169,6 +169,10 @@ DESCRIPTION is the description of the spec."
(2 5 font-lock-type-face)
(8 9 font-lock-keyword-face)))
+ (when-fontifying-it "function literals"
+ ("#(or one two)"
+ (3 4 font-lock-keyword-face)))
+
(when-fontifying-it "should highlight function name in all known forms"
("(letfn [(add [x y]
(+ x y))
diff --git a/test/clojure-ts-mode-indentation-test.el b/test/clojure-ts-mode-indentation-test.el
index e6bbd98..bda3538 100644
--- a/test/clojure-ts-mode-indentation-test.el
+++ b/test/clojure-ts-mode-indentation-test.el
@@ -75,6 +75,38 @@ DESCRIPTION is a string with the description of the spec."
forms))))
+(defmacro when-aligning-it (description &rest forms)
+ "Return a buttercup spec.
+
+Check that all FORMS correspond to properly indented sexps.
+
+DESCRIPTION is a string with the description of the spec."
+ (declare (indent defun))
+ `(it ,description
+ (let ((clojure-ts-align-forms-automatically t)
+ (clojure-ts-align-reader-conditionals t))
+ ,@(mapcar (lambda (form)
+ `(with-temp-buffer
+ (clojure-ts-mode)
+ (insert "\n" ,(replace-regexp-in-string " +" " " form))
+ (indent-region (point-min) (point-max))
+ (should (equal (buffer-substring-no-properties (point-min) (point-max))
+ ,(concat "\n" form)))))
+ forms))
+ (let ((clojure-ts-align-forms-automatically nil))
+ ,@(mapcar (lambda (form)
+ `(with-temp-buffer
+ (clojure-ts-mode)
+ (insert "\n" ,(replace-regexp-in-string " +" " " form))
+ ;; This is to check that we did NOT align anything. Run
+ ;; `indent-region' and then check that no extra spaces
+ ;; where inserted besides the start of the line.
+ (indent-region (point-min) (point-max))
+ (goto-char (point-min))
+ (should-not (search-forward-regexp "\\([^\s\n]\\) +" nil 'noerror))))
+ forms))))
+
+
;; Provide font locking for easier test editing.
(font-lock-add-keywords
@@ -152,6 +184,12 @@ DESCRIPTION is a string with the description of the spec."
(#'foo 5
6)")
+(when-indenting-it "should support function literals"
+ "
+#(or true
+ false
+ %)")
+
(when-indenting-it "should support block-0 expressions"
"
(do (aligned)
@@ -326,3 +364,249 @@ DESCRIPTION is a string with the description of the spec."
(* (twice y) 3))]
(println \"Twice 15 =\" (twice 15))
(println \"Six times 15 =\" (six-times 15)))"))))
+
+(describe "clojure-ts-align"
+ (it "should handle improperly indented content"
+ (with-clojure-ts-buffer-point "
+(let [a-long-name 10
+b |20])"
+ (call-interactively #'clojure-ts-align)
+ (expect (buffer-string) :to-equal "
+(let [a-long-name 10
+ b 20])"))
+
+ (with-clojure-ts-buffer-point "
+(let [^long my-map {:hello \"World\" ;Hello
+ :foo
+ ^String (str \"Foo\" \"Bar\")
+ :number ^long 132
+ :zz \"hello\"}
+ another| {:this ^{:hello \"world\"} \"is\"
+ :a #long \"1234\"
+ :b {:this \"is\"
+ :nested \"map\"}}])"
+ (call-interactively #'clojure-ts-align)
+ (expect (buffer-string) :to-equal "
+(let [^long my-map {:hello \"World\" ;Hello
+ :foo
+ ^String (str \"Foo\" \"Bar\")
+ :number ^long 132
+ :zz \"hello\"}
+ another {:this ^{:hello \"world\"} \"is\"
+ :a #long \"1234\"
+ :b {:this \"is\"
+ :nested \"map\"}}])"))
+
+ (with-clojure-ts-buffer-point "
+(condp = 2
+|123 \"Hello\"
+99999 \"World\"
+234 nil)"
+ (call-interactively #'clojure-ts-align)
+ (expect (buffer-string) :to-equal "
+(condp = 2
+ 123 \"Hello\"
+ 99999 \"World\"
+ 234 nil)")))
+
+ (it "should not align reader conditionals by defaul"
+ (with-clojure-ts-buffer-point "
+#?(:clj 2
+ |:cljs 2)"
+ (call-interactively #'clojure-ts-align)
+ (expect (buffer-string) :to-equal "
+#?(:clj 2
+ :cljs 2)")))
+
+ (it "should align reader conditionals when clojure-ts-align-reader-conditionals is true"
+ (with-clojure-ts-buffer-point "
+#?(:clj 2
+ |:cljs 2)"
+ (setq-local clojure-ts-align-reader-conditionals t)
+ (call-interactively #'clojure-ts-align)
+ (expect (buffer-string) :to-equal "
+#?(:clj 2
+ :cljs 2)")))
+
+ (it "should remove extra commas"
+ (with-clojure-ts-buffer-point "{|:a 2, ,:c 4}"
+ (call-interactively #'clojure-ts-align)
+ (expect (buffer-string) :to-equal "{:a 2, :c 4}"))))
+
+(describe "clojure-ts-align-forms-automatically"
+ ;; Copied from `clojure-mode'
+ (when-aligning-it "should basic forms"
+ "
+{:this-is-a-form b
+ c d}"
+
+ "
+{:this-is b
+ c d}"
+
+ "
+{:this b
+ c d}"
+
+ "
+{:a b
+ c d}"
+
+ "
+(let [this-is-a-form b
+ c d])"
+
+ "
+(let [this-is b
+ c d])"
+
+ "
+(let [this b
+ c d])"
+
+ "
+(let [a b
+ c d])")
+
+ (when-aligning-it "should handle function literals"
+ "
+#(let [hello 1
+ foo \"hone\"]
+ (pringln hello))"
+
+ "
+^{:some :metadata} #(let [foo %
+ bar-zzz %]
+ foo)")
+
+ (when-aligning-it "should handle a blank line"
+ "
+(let [this-is-a-form b
+ c d
+
+ another form
+ k g])"
+
+ "
+{:this-is-a-form b
+ c d
+
+ :another form
+ k g}")
+
+ (when-aligning-it "should handle basic forms (reversed)"
+ "
+{c d
+ :this-is-a-form b}"
+ "
+{c d
+ :this-is b}"
+ "
+{c d
+ :this b}"
+ "
+{c d
+ :a b}"
+
+ "
+(let [c d
+ this-is-a-form b])"
+
+ "
+(let [c d
+ this-is b])"
+
+ "
+(let [c d
+ this b])"
+
+ "
+(let [c d
+ a b])")
+
+ (when-aligning-it "should handle multiple words"
+ "
+(cond this is just
+ a test of
+ how well
+ multiple words will work)")
+
+ (when-aligning-it "should handle nested maps"
+ "
+{:a {:a :a
+ :bbbb :b}
+ :bbbb :b}")
+
+ (when-aligning-it "should regard end as a marker"
+ "
+{:a {:a :a
+ :aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa :a}
+ :b {:a :a
+ :aa :a}}")
+
+ (when-aligning-it "should handle trailing commas"
+ "
+{:a {:a :a,
+ :aa :a},
+ :b {:a :a,
+ :aa :a}}")
+
+ (when-aligning-it "should handle standard reader conditionals"
+ "
+#?(:clj 2
+ :cljs 2)")
+
+ (when-aligning-it "should handle splicing reader conditional"
+ "
+#?@(:clj [2]
+ :cljs [2])")
+
+ (when-aligning-it "should handle sexps broken up by line comments"
+ "
+(let [x 1
+ ;; comment
+ xx 1]
+ xx)"
+
+ "
+{:x 1
+ ;; comment
+ :xxx 2}"
+
+ "
+(case x
+ :aa 1
+ ;; comment
+ :a 2)")
+
+ (when-aligning-it "should work correctly when margin comments appear after nested, multi-line, non-terminal sexps"
+ "
+(let [x {:a 1
+ :b 2} ; comment
+ xx 3]
+ x)"
+
+ "
+{:aa {:b 1
+ :cc 2} ;; comment
+ :a 1}}"
+
+ "
+(case x
+ :a (let [a 1
+ aa (+ a 1)]
+ aa); comment
+ :aa 2)")
+
+ (when-aligning-it "should work correctly when there are ignored forms"
+ "{:map \"with\"
+ :some #_\"ignored\" \"form\"}"
+
+ "{:map \"with\"
+ :multiple \"ignored\"
+ #_#_:forms \"foo\"}")
+
+ (when-aligning-it "should support namespaced maps"
+ "#:hello {:world true
+ :foo \"bar\"
+ :some-very-long \"value\"}"))
diff --git a/test/clojure-ts-mode-refactor-add-arity-test.el b/test/clojure-ts-mode-refactor-add-arity-test.el
new file mode 100644
index 0000000..9c31f27
--- /dev/null
+++ b/test/clojure-ts-mode-refactor-add-arity-test.el
@@ -0,0 +1,350 @@
+;;; clojure-ts-mode-refactor-add-arity-test.el --- Clojure[TS] Mode: refactor add arity test. -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Roman Rudakov
+
+;; Author: Roman Rudakov
+
+;; 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 .
+
+;;; Commentary:
+
+;; Test for `clojure-ts-add-arity'
+
+;;; Code:
+
+(require 'clojure-ts-mode)
+(require 'buttercup)
+(require 'test-helper "test/test-helper")
+
+(describe "clojure-ts-add-arity"
+
+ (when-refactoring-with-point-it "should add an arity to a single-arity defn with args on same line"
+ "(defn foo [arg]
+ body|)"
+
+ "(defn foo
+ ([|])
+ ([arg]
+ body))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should add an arity to a single-arity defn with args on next line"
+ "(defn foo
+ [arg]
+ bo|dy)"
+
+ "(defn foo
+ ([|])
+ ([arg]
+ body))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should handle a single-arity defn with a docstring"
+ "(defn foo
+ \"some docst|ring\"
+ [arg]
+ body)"
+
+ "(defn foo
+ \"some docstring\"
+ ([|])
+ ([arg]
+ body))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should handle a single-arity defn with metadata"
+ "(defn fo|o
+ ^{:bla \"meta\"}
+ [arg]
+ body)"
+
+ "(defn foo
+ ^{:bla \"meta\"}
+ ([|])
+ ([arg]
+ body))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should add an arity to a multi-arity defn"
+ "(defn foo
+ ([arg1])
+ ([ar|g1 arg2]
+ body))"
+
+ "(defn foo
+ ([|])
+ ([arg1])
+ ([arg1 arg2]
+ body))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should handle a multi-arity defn with a docstring"
+ "(defn foo
+ \"some docstring\"
+ ([])
+ ([arg|]
+ body))"
+
+ "(defn foo
+ \"some docstring\"
+ ([|])
+ ([])
+ ([arg]
+ body))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should handle a multi-arity defn with metadata"
+ "(defn foo
+ \"some docstring\"
+ ^{:bla \"meta\"}
+ ([])
+ |([arg]
+ body))"
+
+ "(defn foo
+ \"some docstring\"
+ ^{:bla \"meta\"}
+ ([|])
+ ([])
+ ([arg]
+ body))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should handle a single-arity fn"
+ "(fn foo [arg]
+ body|)"
+
+ "(fn foo
+ ([|])
+ ([arg]
+ body))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should handle a multi-arity fn"
+ "(fn foo
+ ([x y]
+ body)
+ ([a|rg]
+ body))"
+
+ "(fn foo
+ ([|])
+ ([x y]
+ body)
+ ([arg]
+ body))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should handle a single-arity defmacro"
+ "(defmacro foo [arg]
+ body|)"
+
+ "(defmacro foo
+ ([|])
+ ([arg]
+ body))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should handle a multi-arity defmacro"
+ "(defmacro foo
+ ([x y]
+ body)
+ ([a|rg]
+ body))"
+
+ "(defmacro foo
+ ([|])
+ ([x y]
+ body)
+ ([arg]
+ body))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should handle a single-arity defmethod"
+ "(defmethod foo :bar [arg]
+ body|)"
+
+ "(defmethod foo :bar
+ ([|])
+ ([arg]
+ body))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should handle a multi-arity defmethod"
+ "(defmethod foo :bar
+ ([x y]
+ body)
+ ([a|rg]
+ body))"
+
+ "(defmethod foo :bar
+ ([|])
+ ([x y]
+ body)
+ ([arg]
+ body))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should handle a defn inside a reader conditional"
+ "#?(:clj
+ (defn foo
+ \"some docstring\"
+ ^{:bla \"meta\"}
+ |([arg]
+ body)))"
+
+ "#?(:clj
+ (defn foo
+ \"some docstring\"
+ ^{:bla \"meta\"}
+ ([|])
+ ([arg]
+ body)))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should handle a defn inside a reader conditional with 2 platform tags"
+ "#?(:clj
+ (defn foo
+ \"some docstring\"
+ ^{:bla \"meta\"}
+ |([arg]
+ body))
+ :cljs
+ (defn foo
+ \"some docstring\"
+ ^{:bla \"meta\"}
+ ([arg]
+ body)))"
+
+ "#?(:clj
+ (defn foo
+ \"some docstring\"
+ ^{:bla \"meta\"}
+ ([|])
+ ([arg]
+ body))
+ :cljs
+ (defn foo
+ \"some docstring\"
+ ^{:bla \"meta\"}
+ ([arg]
+ body)))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should handle a single-arity fn inside a letfn"
+ "(letfn [(foo [x]
+ bo|dy)]
+ (foo 3))"
+
+ "(letfn [(foo
+ ([|])
+ ([x]
+ body))]
+ (foo 3))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should handle a multi-arity fn inside a letfn"
+ "(letfn [(foo
+ ([x]
+ body)
+ |([x y]
+ body))]
+ (foo 3))"
+
+ "(letfn [(foo
+ ([|])
+ ([x]
+ body)
+ ([x y]
+ body))]
+ (foo 3))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should handle a proxy"
+ "(proxy [Foo] []
+ (bar [arg]
+ body|))"
+
+ "(proxy [Foo] []
+ (bar
+ ([|])
+ ([arg]
+ body)))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should handle a defprotocol"
+ "(defprotocol Foo
+ \"some docstring\"
+ (bar [arg] [x |y] \"some docstring\"))"
+
+ "(defprotocol Foo
+ \"some docstring\"
+ (bar [|] [arg] [x y] \"some docstring\"))"
+
+ (clojure-ts-add-arity))
+
+ (when-refactoring-with-point-it "should handle a reify"
+ "(reify Foo
+ (bar [arg] body)
+ (blahs [arg]| body))"
+
+ "(reify Foo
+ (bar [arg] body)
+ (blahs [|])
+ (blahs [arg] body))"
+
+ (clojure-ts-add-arity))
+
+ (it "should signal a user error when point is not inside a function body"
+ (with-clojure-ts-buffer-point "
+(letf|n [(foo
+ ([x]
+ body)
+ ([x y]
+ body))]
+ (foo 3))"
+ (expect (clojure-ts-add-arity)
+ :to-throw
+ 'user-error
+ '("No suitable form to add an arity at point")))
+
+ (with-clojure-ts-buffer-point "
+(defprotocol Fo|o
+ \"some docstring\"
+ (bar [arg] [x y] \"some docstring\"))"
+ (expect (clojure-ts-add-arity)
+ :to-throw
+ 'user-error
+ '("No suitable form to add an arity at point")))))
+
+(provide 'clojure-ts-mode-refactor-add-arity-test)
+;;; clojure-ts-mode-refactor-add-arity-test.el ends here
diff --git a/test/clojure-ts-mode-refactor-threading-test.el b/test/clojure-ts-mode-refactor-threading-test.el
new file mode 100644
index 0000000..ce26d5d
--- /dev/null
+++ b/test/clojure-ts-mode-refactor-threading-test.el
@@ -0,0 +1,443 @@
+;;; clojure-ts-mode-refactor-threading-test.el --- clojure-ts-mode: refactor threading tests -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Roman Rudakov
+
+;; Author: Roman Rudakov
+;; 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 .
+
+;;; Commentary:
+
+;; The threading refactoring code is adapted from clojure-mode.el.
+
+;;; Code:
+
+(require 'clojure-ts-mode)
+(require 'buttercup)
+(require 'test-helper "test/test-helper")
+
+(describe "clojure-ts-thread"
+
+ (when-refactoring-it "should work with -> when performed once"
+ "(-> (dissoc (assoc {} :key \"value\") :lock))"
+
+ "(-> (assoc {} :key \"value\")
+ (dissoc :lock))"
+
+ (clojure-ts-thread))
+
+ (when-refactoring-it "should work with -> when performed twice"
+ "(-> (dissoc (assoc {} :key \"value\") :lock))"
+
+ "(-> {}
+ (assoc :key \"value\")
+ (dissoc :lock))"
+
+ (clojure-ts-thread)
+ (clojure-ts-thread))
+
+ (when-refactoring-it "should not thread maps"
+ "(-> (dissoc (assoc {} :key \"value\") :lock))"
+
+ "(-> {}
+ (assoc :key \"value\")
+ (dissoc :lock))"
+
+ (clojure-ts-thread)
+ (clojure-ts-thread)
+ (clojure-ts-thread))
+
+ (when-refactoring-it "should not thread last sexp"
+ "(-> (dissoc (assoc (get-a-map) :key \"value\") :lock))"
+
+ "(-> (get-a-map)
+ (assoc :key \"value\")
+ (dissoc :lock))"
+
+ (clojure-ts-thread)
+ (clojure-ts-thread)
+ (clojure-ts-thread))
+
+ (when-refactoring-it "should thread-first-easy-on-whitespace"
+ "(->
+ (dissoc (assoc {} :key \"value\") :lock))"
+
+ "(->
+ (assoc {} :key \"value\")
+ (dissoc :lock))"
+
+ (clojure-ts-thread))
+
+ (when-refactoring-it "should remove superfluous parens"
+ "(-> (square (sum [1 2 3 4 5])))"
+
+ "(-> [1 2 3 4 5]
+ sum
+ square)"
+
+ (clojure-ts-thread)
+ (clojure-ts-thread))
+
+ (when-refactoring-it "should work with cursor before ->"
+ "(-> (not (s-acc/mobile? session)))"
+
+ "(-> (s-acc/mobile? session)
+ not)"
+
+ (beginning-of-buffer)
+ (clojure-ts-thread))
+
+ (when-refactoring-it "should work with one step with ->>"
+ "(->> (map square (filter even? [1 2 3 4 5])))"
+
+ "(->> (filter even? [1 2 3 4 5])
+ (map square))"
+
+ (clojure-ts-thread))
+
+ (when-refactoring-it "should work with two steps with ->>"
+ "(->> (map square (filter even? [1 2 3 4 5])))"
+
+ "(->> [1 2 3 4 5]
+ (filter even?)
+ (map square))"
+
+ (clojure-ts-thread)
+ (clojure-ts-thread))
+
+ (when-refactoring-it "should not thread vectors with ->>"
+ "(->> (map square (filter even? [1 2 3 4 5])))"
+
+ "(->> [1 2 3 4 5]
+ (filter even?)
+ (map square))"
+
+ (clojure-ts-thread)
+ (clojure-ts-thread)
+ (clojure-ts-thread))
+
+ (when-refactoring-it "should not thread last sexp with ->>"
+ "(->> (map square (filter even? (get-a-list))))"
+
+ "(->> (get-a-list)
+ (filter even?)
+ (map square))"
+
+ (clojure-ts-thread)
+ (clojure-ts-thread)
+ (clojure-ts-thread))
+
+ (when-refactoring-it "should work with some->"
+ "(some-> (+ (val (find {:a 1} :b)) 5))"
+
+ "(some-> {:a 1}
+ (find :b)
+ val
+ (+ 5))"
+
+ (clojure-ts-thread)
+ (clojure-ts-thread)
+ (clojure-ts-thread))
+
+ (when-refactoring-it "should work with some->>"
+ "(some->> (+ 5 (val (find {:a 1} :b))))"
+
+ "(some->> :b
+ (find {:a 1})
+ val
+ (+ 5))"
+
+ (clojure-ts-thread)
+ (clojure-ts-thread)
+ (clojure-ts-thread)))
+
+(describe "clojure-ts-unwind"
+
+ (when-refactoring-it "should unwind -> one step"
+ "(-> {}
+ (assoc :key \"value\")
+ (dissoc :lock))"
+
+ "(-> (assoc {} :key \"value\")
+ (dissoc :lock))"
+
+ (clojure-ts-unwind))
+
+ (when-refactoring-it "should unwind -> completely"
+ "(-> {}
+ (assoc :key \"value\")
+ (dissoc :lock))"
+
+ "(dissoc (assoc {} :key \"value\") :lock)"
+
+ (clojure-ts-unwind)
+ (clojure-ts-unwind))
+
+ (when-refactoring-it "should unwind ->> one step"
+ "(->> [1 2 3 4 5]
+ (filter even?)
+ (map square))"
+
+ "(->> (filter even? [1 2 3 4 5])
+ (map square))"
+
+ (clojure-ts-unwind))
+
+ (when-refactoring-it "should unwind ->> completely"
+ "(->> [1 2 3 4 5]
+ (filter even?)
+ (map square))"
+
+ "(map square (filter even? [1 2 3 4 5]))"
+
+ (clojure-ts-unwind)
+ (clojure-ts-unwind))
+
+ (when-refactoring-it "should unwind N steps with numeric prefix arg"
+ "(->> [1 2 3 4 5]
+ (filter even?)
+ (map square)
+ sum)"
+
+ "(->> (map square (filter even? [1 2 3 4 5]))
+ sum)"
+
+ (clojure-ts-unwind 2))
+
+ (when-refactoring-it "should unwind completely with universal prefix arg"
+ "(->> [1 2 3 4 5]
+ (filter even?)
+ (map square)
+ sum)"
+
+ "(sum (map square (filter even? [1 2 3 4 5])))"
+
+ (clojure-ts-unwind '(4)))
+
+ (when-refactoring-it "should unwind correctly when multiple ->> are present on same line"
+ "(->> 1 inc) (->> [1 2 3 4 5]
+ (filter even?)
+ (map square))"
+
+ "(->> 1 inc) (map square (filter even? [1 2 3 4 5]))"
+
+ (clojure-ts-unwind)
+ (clojure-ts-unwind))
+
+ (when-refactoring-it "should unwind with function name"
+ "(->> [1 2 3 4 5]
+ sum
+ square)"
+
+ "(->> (sum [1 2 3 4 5])
+ square)"
+
+ (clojure-ts-unwind))
+
+ (when-refactoring-it "should unwind with function name twice"
+ "(-> [1 2 3 4 5]
+ sum
+ square)"
+
+ "(square (sum [1 2 3 4 5]))"
+
+ (clojure-ts-unwind)
+ (clojure-ts-unwind))
+
+ (when-refactoring-it "should thread-issue-6-1"
+ "(defn plus [a b]
+ (-> a (+ b)))"
+
+ "(defn plus [a b]
+ (+ a b))"
+
+ (clojure-ts-unwind))
+
+ (when-refactoring-it "should thread-issue-6-2"
+ "(defn plus [a b]
+ (->> a (+ b)))"
+
+ "(defn plus [a b]
+ (+ b a))"
+
+ (clojure-ts-unwind))
+
+ (when-refactoring-it "should unwind some->"
+ "(some-> {:a 1}
+ (find :b)
+ val
+ (+ 5))"
+
+ "(some-> (val (find {:a 1} :b))
+ (+ 5))"
+
+ (clojure-ts-unwind)
+ (clojure-ts-unwind))
+
+ (when-refactoring-it "should unwind some->>"
+ "(some->> :b
+ (find {:a 1}) val
+ (+ 5))"
+
+ "(some->> (val (find {:a 1} :b))
+ (+ 5))"
+
+ (clojure-ts-unwind)
+ (clojure-ts-unwind)))
+
+(describe "clojure-ts-thread-first-all"
+
+ (when-refactoring-it "should thread first all sexps"
+ "(->map (assoc {} :key \"value\") :lock)"
+
+ "(-> {}
+ (assoc :key \"value\")
+ (->map :lock))"
+
+ (beginning-of-buffer)
+ (clojure-ts-thread-first-all nil))
+
+ (when-refactoring-it "should thread a form except the last expression"
+ "(->map (assoc {} :key \"value\") :lock)"
+
+ "(-> (assoc {} :key \"value\")
+ (->map :lock))"
+
+ (beginning-of-buffer)
+ (clojure-ts-thread-first-all t))
+
+ (when-refactoring-it "should thread with an empty first line"
+ "(map
+ inc
+ [1 2])"
+
+ "(-> inc
+ (map
+ [1 2]))"
+
+ (goto-char (point-min))
+ (clojure-ts-thread-first-all nil))
+
+ (when-refactoring-it "should thread-first-maybe-unjoin-lines"
+ "(map
+ inc
+ [1 2])"
+
+ "(map
+ inc
+ [1 2])"
+
+ (goto-char (point-min))
+ (clojure-ts-thread-first-all nil)
+ (clojure-ts-unwind-all)))
+
+(describe "clojure-ts-thread-last-all"
+
+ (when-refactoring-it "should fully thread a form"
+ "(map square (filter even? (make-things)))"
+
+ "(->> (make-things)
+ (filter even?)
+ (map square))"
+
+ (beginning-of-buffer)
+ (clojure-ts-thread-last-all nil))
+
+ (when-refactoring-it "should thread a form except the last expression"
+ "(map square (filter even? (make-things)))"
+
+ "(->> (filter even? (make-things))
+ (map square))"
+
+ (beginning-of-buffer)
+ (clojure-ts-thread-last-all t))
+
+ (when-refactoring-it "should handle dangling parens 1"
+ "(map inc
+ (range))"
+
+ "(->> (range)
+ (map inc))"
+
+ (beginning-of-buffer)
+ (clojure-ts-thread-last-all nil))
+
+ (when-refactoring-it "should handle dangling parens 2"
+ "(deftask dev []
+ (comp (serve)
+ (cljs)))"
+
+ "(->> (cljs)
+ (comp (serve))
+ (deftask dev []))"
+
+ (beginning-of-buffer)
+ (clojure-ts-thread-last-all nil)))
+
+(describe "clojure-ts-unwind-all"
+
+ (when-refactoring-it "should unwind all in ->"
+ "(-> {}
+ (assoc :key \"value\")
+ (dissoc :lock))"
+
+ "(dissoc (assoc {} :key \"value\") :lock)"
+
+ (beginning-of-buffer)
+ (clojure-ts-unwind-all))
+
+ (when-refactoring-it "should unwind all in ->>"
+ "(->> (make-things)
+ (filter even?)
+ (map square))"
+
+ "(map square (filter even? (make-things)))"
+
+ (beginning-of-buffer)
+ (clojure-ts-unwind-all))
+
+ (when-refactoring-it "should leave multiline sexp alone"
+ "(->> [a b]
+ (some (fn [x]
+ (when x
+ 10))))"
+
+ "(some (fn [x]
+ (when x
+ 10))
+ [a b])"
+
+ (clojure-ts-unwind-all))
+
+ ;; NOTE: This feature is implemented in `clojure-mode' via text properties and
+ ;; doesn't work for the same expression after restarting Emacs. For now it's
+ ;; not implemented in `clojure-ts-mode', although we respect multiline
+ ;; expressions in some cases.
+ ;;
+ ;; (when-refactoring-it "should thread-last-maybe-unjoin-lines" "(deftask dev
+ ;; [] (comp (serve) (cljs (lala) 10)))"
+
+ ;; "(deftask dev []
+ ;; (comp (serve)
+ ;; (cljs (lala)
+ ;; 10)))"
+
+ ;; (goto-char (point-min))
+ ;; (clojure-ts-thread-last-all nil)
+ ;; (clojure-ts-unwind-all))
+ )
+
+(provide 'clojure-ts-mode-refactor-threading-test)
+;;; clojure-ts-mode-refactor-threading-test.el ends here
diff --git a/test/clojure-ts-mode-util-test.el b/test/clojure-ts-mode-util-test.el
index 8156c1a..05b0fcc 100644
--- a/test/clojure-ts-mode-util-test.el
+++ b/test/clojure-ts-mode-util-test.el
@@ -31,101 +31,101 @@
(describe "clojure-ts-find-ns"
(it "should find common namespace declarations"
(with-clojure-ts-buffer "(ns foo)"
- (expect (clojure-ts-find-ns) :to-equal "foo"))
+ (expect (clojure-ts-find-ns) :to-equal "foo"))
(with-clojure-ts-buffer "(ns
foo)"
- (expect (clojure-ts-find-ns) :to-equal "foo"))
+ (expect (clojure-ts-find-ns) :to-equal "foo"))
(with-clojure-ts-buffer "(ns foo.baz)"
- (expect (clojure-ts-find-ns) :to-equal "foo.baz"))
+ (expect (clojure-ts-find-ns) :to-equal "foo.baz"))
(with-clojure-ts-buffer "(ns ^:bar foo)"
- (expect (clojure-ts-find-ns) :to-equal "foo"))
+ (expect (clojure-ts-find-ns) :to-equal "foo"))
(with-clojure-ts-buffer "(ns ^:bar ^:baz foo)"
- (expect (clojure-ts-find-ns) :to-equal "foo")))
+ (expect (clojure-ts-find-ns) :to-equal "foo")))
(it "should find namespaces with spaces before ns form"
(with-clojure-ts-buffer " (ns foo)"
- (expect (clojure-ts-find-ns) :to-equal "foo")))
+ (expect (clojure-ts-find-ns) :to-equal "foo")))
(it "should skip namespaces within any comment forms"
(with-clojure-ts-buffer "(comment
(ns foo))"
- (expect (clojure-ts-find-ns) :to-equal nil))
+ (expect (clojure-ts-find-ns) :to-equal nil))
(with-clojure-ts-buffer " (ns foo)
(comment
(ns bar))"
- (expect (clojure-ts-find-ns) :to-equal "foo"))
+ (expect (clojure-ts-find-ns) :to-equal "foo"))
(with-clojure-ts-buffer " (comment
(ns foo))
(ns bar)
(comment
(ns baz))"
- (expect (clojure-ts-find-ns) :to-equal "bar")))
+ (expect (clojure-ts-find-ns) :to-equal "bar")))
(it "should find namespace declarations with nested metadata and docstrings"
(with-clojure-ts-buffer "(ns ^{:bar true} foo)"
- (expect (clojure-ts-find-ns) :to-equal "foo"))
+ (expect (clojure-ts-find-ns) :to-equal "foo"))
(with-clojure-ts-buffer "(ns #^{:bar true} foo)"
- (expect (clojure-ts-find-ns) :to-equal "foo"))
+ (expect (clojure-ts-find-ns) :to-equal "foo"))
(with-clojure-ts-buffer "(ns #^{:fail {}} foo)"
- (expect (clojure-ts-find-ns) :to-equal "foo"))
+ (expect (clojure-ts-find-ns) :to-equal "foo"))
(with-clojure-ts-buffer "(ns ^{:fail2 {}} foo.baz)"
- (expect (clojure-ts-find-ns) :to-equal "foo.baz"))
+ (expect (clojure-ts-find-ns) :to-equal "foo.baz"))
(with-clojure-ts-buffer "(ns ^{} foo)"
- (expect (clojure-ts-find-ns) :to-equal "foo"))
+ (expect (clojure-ts-find-ns) :to-equal "foo"))
(with-clojure-ts-buffer "(ns ^{:skip-wiki true}
aleph.netty)"
- (expect (clojure-ts-find-ns) :to-equal "aleph.netty"))
+ (expect (clojure-ts-find-ns) :to-equal "aleph.netty"))
(with-clojure-ts-buffer "(ns ^{:foo {:bar :baz} :fake (ns in.meta)} foo
\"docstring
(ns misleading)\")"
- (expect (clojure-ts-find-ns) :to-equal "foo")))
+ (expect (clojure-ts-find-ns) :to-equal "foo")))
(it "should support non-alphanumeric characters"
(with-clojure-ts-buffer "(ns foo+)"
- (expect (clojure-ts-find-ns) :to-equal "foo+"))
+ (expect (clojure-ts-find-ns) :to-equal "foo+"))
(with-clojure-ts-buffer "(ns bar**baz$-_quux)"
- (expect (clojure-ts-find-ns) :to-equal "bar**baz$-_quux"))
+ (expect (clojure-ts-find-ns) :to-equal "bar**baz$-_quux"))
(with-clojure-ts-buffer "(ns aoc-2019.puzzles.day14)"
- (expect (clojure-ts-find-ns) :to-equal "aoc-2019.puzzles.day14")))
+ (expect (clojure-ts-find-ns) :to-equal "aoc-2019.puzzles.day14")))
(it "should support in-ns forms"
(with-clojure-ts-buffer "(in-ns 'bar.baz)"
- (expect (clojure-ts-find-ns) :to-equal "bar.baz")))
+ (expect (clojure-ts-find-ns) :to-equal "bar.baz")))
(it "should take the first ns instead of closest unlike clojure-mode"
(with-clojure-ts-buffer " (ns foo1)
(ns foo2)"
- (expect (clojure-ts-find-ns) :to-equal "foo1"))
+ (expect (clojure-ts-find-ns) :to-equal "foo1"))
(with-clojure-ts-buffer-point " (in-ns foo1)
(ns 'foo2)
(in-ns 'foo3)
|
(ns foo4)"
- (expect (clojure-ts-find-ns) :to-equal "foo3"))
+ (expect (clojure-ts-find-ns) :to-equal "foo3"))
(with-clojure-ts-buffer "(ns foo)
(ns-unmap *ns* 'map)
(ns.misleading 1 2 3)"
- (expect (clojure-ts-find-ns) :to-equal "foo")))
+ (expect (clojure-ts-find-ns) :to-equal "foo")))
(it "should skip leading garbage"
(with-clojure-ts-buffer " (ns foo)"
- (expect (clojure-ts-find-ns) :to-equal "foo"))
+ (expect (clojure-ts-find-ns) :to-equal "foo"))
(with-clojure-ts-buffer "1(ns foo)"
- (expect (clojure-ts-find-ns) :to-equal "foo"))
+ (expect (clojure-ts-find-ns) :to-equal "foo"))
(with-clojure-ts-buffer "1 (ns foo)"
- (expect (clojure-ts-find-ns) :to-equal "foo"))
+ (expect (clojure-ts-find-ns) :to-equal "foo"))
(with-clojure-ts-buffer "1
(ns foo)"
- (expect (clojure-ts-find-ns) :to-equal "foo"))
+ (expect (clojure-ts-find-ns) :to-equal "foo"))
(with-clojure-ts-buffer "[1]
(ns foo)"
- (expect (clojure-ts-find-ns) :to-equal "foo"))
+ (expect (clojure-ts-find-ns) :to-equal "foo"))
(with-clojure-ts-buffer "[1] (ns foo)"
- (expect (clojure-ts-find-ns) :to-equal "foo"))
+ (expect (clojure-ts-find-ns) :to-equal "foo"))
(with-clojure-ts-buffer "[1](ns foo)"
- (expect (clojure-ts-find-ns) :to-equal "foo"))
+ (expect (clojure-ts-find-ns) :to-equal "foo"))
(with-clojure-ts-buffer "(ns)(ns foo)"
- (expect (clojure-ts-find-ns) :to-equal "foo"))
+ (expect (clojure-ts-find-ns) :to-equal "foo"))
(with-clojure-ts-buffer "(ns 'foo)(ns bar)"
- (expect (clojure-ts-find-ns) :to-equal "bar"))))
+ (expect (clojure-ts-find-ns) :to-equal "bar"))))
diff --git a/test/samples/align.clj b/test/samples/align.clj
new file mode 100644
index 0000000..b7933f3
--- /dev/null
+++ b/test/samples/align.clj
@@ -0,0 +1,64 @@
+(ns align)
+
+(let [^long my-map {:hello "World" ;Hello
+ :foo
+ ^String (str "Foo" "Bar")
+ :number ^long 132
+ :zz "hello"}
+ another {:this ^{:hello "world"} "is"
+ :a #long "1234"
+ :b {:this "is"
+ :nested "map"}}])
+
+
+{:foo "bar", :baz "Hello"
+ :a "b" :c "d"}
+
+
+(clojure.core/with-redefs [hello "world"
+ foo "bar"]
+ (println hello foo))
+
+(condp = 2
+ 123 "Hello"
+ 99999 "World"
+ 234 nil)
+
+(let [a-long-name 10
+ b 20])
+
+#?(:clj 2
+ :cljs 2)
+
+#?@(:clj [2]
+ :cljs [4])
+
+(let [this-is-a-form b
+ c d
+
+ another form
+ k g])
+
+{:this-is-a-form b
+ c d
+
+ :another form
+ k g}
+
+(let [x {:a 1
+ :b 2} ; comment
+ xx 3]
+ x)
+
+(case x
+ :a (let [a 1
+ aa (+ a 1)]
+ aa); comment
+ :aa 2)
+
+{:map "with"
+ :some #_"ignored" "form"}
+
+{:map "with"
+ :multiple "ignored"
+ #_#_:forms "foo"}
diff --git a/test/samples/indentation.clj b/test/samples/indentation.clj
index 53e8269..132a5f2 100644
--- a/test/samples/indentation.clj
+++ b/test/samples/indentation.clj
@@ -281,3 +281,13 @@
user "John Doe"]
(dotimes [_ (add x y)]
(hello user))))
+
+(with-open [input-stream
+ ^java.io.BufferedInputStream
+ (foo bar
+ baz
+ true)
+
+ reader
+ (io/reader input-stream)]
+ (read-report (into [] (csv/read-csv reader))))
diff --git a/test/samples/outline.clj b/test/samples/outline.clj
new file mode 100644
index 0000000..b6722d2
--- /dev/null
+++ b/test/samples/outline.clj
@@ -0,0 +1,19 @@
+(ns outline)
+
+
+;;; First heading level 1
+
+(defn foo
+ [bar]
+ (println bar))
+
+;;;; Heading level 2
+
+(def baz
+ {:hello "World"})
+
+;;; Second heading level 1
+
+(defn hello-world
+ []
+ (println "Hello, world!"))
diff --git a/test/samples/refactoring.clj b/test/samples/refactoring.clj
new file mode 100644
index 0000000..10f12b5
--- /dev/null
+++ b/test/samples/refactoring.clj
@@ -0,0 +1,143 @@
+(ns refactoring)
+
+;;; Threading
+
+;;;; Unwind
+
+(-> ;; This is comment
+ (foo)
+ ;; Another comment
+ (bar true
+ ;; Hello
+ false)
+ (baz))
+
+
+(let [some (->> yeah
+ (world foo
+ false)
+ hello)])
+
+(->> coll
+ (filter identity)
+ (map :id)
+ (map :name))
+
+(some->> coll
+ (filter identity)
+ (map :id)
+ (map :name))
+
+(defn plus [a b]
+ (-> a (+ b)))
+
+(some->> :b
+ (find {:a 1}) val
+ (+ 5))
+
+(some->> (val (find {:a 1} :b))
+ (+ 5))
+
+;;;; Thread
+
+(-> (foo (bar (baz)) "arg on a separate line"))
+
+(foo (bar (baz)))
+
+(-> (foo (bar))
+ (baz))
+
+(->> (filter :active? (map :status items)))
+
+(-> (dissoc (assoc {} :key "value") :lock))
+
+
+(-> inc
+ (map [1 2]))
+
+(map
+ inc
+ [1 2])
+
+#(-> (.-value (.-target %)))
+
+(->> (range)
+ (map inc))
+
+(->> (map square (filter even? [1 2 3 4 5])))
+
+(-> (dissoc (assoc {} :key "value") :lock))
+
+(deftask dev []
+ (comp (serve)
+ (cljs (lala)
+ 10)))
+
+(def my-name "Roma")
+
+(defn say-hello
+ []
+ (println "Hello" my-name))
+
+(definline bad-sqr [x] `(* ~x ~x))
+
+(defmulti service-charge (juxt account-level :tag))
+
+;; Convert collections.
+
+#{1 2 3}
+
+[1 2 3]
+
+#:hello {:world true
+ :foo "bar"
+ :some-very-long "value"}
+
+{:name "Roma"
+ :foo true}
+
+(reify
+ java.io.FileFilter
+ (accept [this f]
+ (.isDirectory f))
+
+ (hello [world]
+ false))
+
+(defmulti which-color-mm (fn [m & args] (:color m)))
+(defmethod which-color-mm :blue
+ ([m] (print m))
+ ([m f] (f m)))
+
+(letfn [(twice [x]
+ (* x 2))
+ (six-times [y]
+ (* (twice y) 3))]
+ (println "Twice 15 =" (twice 15))
+ (println "Six times 15 =" (six-times 15)))
+
+(let [p (proxy [java.io.InputStream] []
+ (read
+ ([] 1)
+ ([^bytes bytes] 2)
+ ([^bytes bytes off len] 3)))]
+ (println (.read p))
+ (println (.read p (byte-array 3)))
+ (println (.read p (byte-array 3) 0 3)))
+
+(defprotocol Fly
+ "A simple protocol for flying"
+ (fly [this]
+ "Method to fly"))
+
+(defn foo
+ ^{:bla "meta"}
+ [arg]
+ body)
+
+(if ^boolean (= 2 2)
+ true
+ false)
+
+(when-not true
+ (println "Hello world"))
diff --git a/test/samples/regex.clj b/test/samples/regex.clj
new file mode 100644
index 0000000..f37b50a
--- /dev/null
+++ b/test/samples/regex.clj
@@ -0,0 +1,7 @@
+(ns regex)
+
+(def email-pattern
+ #"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?")
+
+(def simple-regex
+ #"^(\\d+).*[a|b|c|d].*[a-z0-9!#]$")
diff --git a/test/samples/test.clj b/test/samples/test.clj
index 842ff5a..18ead86 100644
--- a/test/samples/test.clj
+++ b/test/samples/test.clj
@@ -41,6 +41,19 @@
0 0i)
+;; Function literals
+
+^{:some "metadata"} #(let [foo %
+ bar-zzz %]
+ foo)
+
+#(or one
+ two)
+
+#(let [hello 1
+ foo "hone"]
+ (pringln hello))
+
;; examples of valid namespace definitions
(comment
(ns .validns)
diff --git a/test/test-helper.el b/test/test-helper.el
index f363644..fa821e6 100644
--- a/test/test-helper.el
+++ b/test/test-helper.el
@@ -39,13 +39,13 @@ And evaluate BODY."
TEXT is a string with a | indicating where point is. The | will be erased
and point left there."
- (declare (indent 2))
+ (declare (indent 1))
`(progn
(with-clojure-ts-buffer ,text
- (goto-char (point-min))
- (re-search-forward "|")
- (delete-char -1)
- ,@body)))
+ (goto-char (point-min))
+ (re-search-forward "|")
+ (delete-char -1)
+ ,@body)))
(defun clojure-ts--s-index-of (needle s &optional ignore-case)
"Returns first index of NEEDLE in S, or nil.
@@ -108,4 +108,5 @@ Removes the temp directory at the end of evaluation."
,@body)
(delete-directory ,temp-dir t))))
+(provide 'test-helper)
;;; test-helper.el ends here