From 155cb1861a31281a3b3b9cbcfd86cd35989a5fc3 Mon Sep 17 00:00:00 2001 From: Roman Rudakov Date: Mon, 5 May 2025 21:27:54 +0200 Subject: [PATCH] Introduce commands to convert collection type --- CHANGELOG.md | 1 + README.md | 34 +++-- clojure-ts-mode.el | 87 +++++++++++++ ...clojure-ts-mode-convert-collection-test.el | 119 ++++++++++++++++++ test/samples/refactoring.clj | 10 ++ 5 files changed, 242 insertions(+), 9 deletions(-) create mode 100644 test/clojure-ts-mode-convert-collection-test.el diff --git a/CHANGELOG.md b/CHANGELOG.md index 292cbe0..f2413e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/README.md b/README.md index 4ef3293..0891515 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el index 4ce9a29..204126c 100644 --- a/clojure-ts-mode.el +++ b/clojure-ts-mode.el @@ -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) @@ -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.") @@ -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] 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/samples/refactoring.clj b/test/samples/refactoring.clj index 109243d..d06a77d 100644 --- a/test/samples/refactoring.clj +++ b/test/samples/refactoring.clj @@ -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}