Skip to content

Introduce commands to convert collection type #92

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
`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.

## 0.3.0 (2025-04-15)

Expand Down
34 changes: 25 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,17 +400,33 @@ vice versa.
explicitly with setting `clojure-ts-use-metadata-for-defn-privacy` to `t` for
`defn`s too.

### 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`

### 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` |
| 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` |

### Customize refactoring commands prefix

Expand Down
87 changes: 87 additions & 0 deletions clojure-ts-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -2048,6 +2048,77 @@ value is `clojure-ts-thread-all-but-last'."
(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 ?{ ?#))

(defvar clojure-ts-refactor-map
(let ((map (make-sparse-keymap)))
(keymap-set map "C-t" #'clojure-ts-thread)
Expand All @@ -2060,6 +2131,16 @@ value is `clojure-ts-thread-all-but-last'."
(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)
map)
"Keymap for `clojure-ts-mode' refactoring commands.")

Expand All @@ -2074,6 +2155,12 @@ value is `clojure-ts-thread-all-but-last'."
["Toggle between string & keyword" clojure-ts-cycle-keyword-string]
["Align expression" clojure-ts-align]
["Cycle privacy" clojure-ts-cycle-privacy]
("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]
Expand Down
119 changes: 119 additions & 0 deletions test/clojure-ts-mode-convert-collection-test.el
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>

;; 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 <https://www.gnu.org/licenses/>.

;;; 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
10 changes: 10 additions & 0 deletions test/samples/refactoring.clj
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,13 @@
(definline bad-sqr [x] `(* ~x ~x))

(defmulti service-charge (juxt account-level :tag))

;; Convert collections.

#{1 2 3}

[1 2 3]

;; TODO: Define indentation rule for `ns_map_lit`
#:hello{:name "Roma"
:world true}