Fork me on GitHub
#rewrite-clj
<
2021-11-25
>
ericdallo00:11:15

Any tips on how to sort a map node by its keys alphabetically with rewrite-clj?

ericdallo00:11:03

Like:

(-> (z/of-string "{:b 1 :a 2}")
     my-sort
     z/sexpr)
=> {:a 2 :b 1}

lread04:11:51

Should not be crazy hard.

lread04:11:29

Can take a look sometime my-tomorrow, if you like

lread04:11:59

Have you considered what you want to do with reader discards #_?

lread04:11:48

And maps can be unbalanced in rewrite-clj. Any thoughts there?

borkdude07:11:33

And … namespaced maps

ericdallo12:11:36

Yeah, it'd be necessary to handle those case I think

ericdallo12:11:56

Is there any other easier way to sort that if not using rewrite-clj?

lread14:11:43

You might also have to think about formatting. Does sorting also reformat? Like if you have:

{:b 2 :c 3
 :a 1}
What result would you expect?

lread14:11:20

And what would you expect to happen for nested maps? Just immediate children sorted?

lread14:11:49

I think I'd use rewrite-clj if I were to do this, but... maybe I am biased?

ericdallo14:11:08

I would expect to keep the same sort, follow the same formatting before applying the function

ericdallo14:11:37

> I think I'd use rewrite-clj if I were to do this, but... maybe I am biased? hahah, I think rewrite-clj makes sense for that, but It doesn't seems easy at least to me

lread14:11:20

You could decide not to sort if there are #_ or the map is unbalanced.

👍 1
lread14:11:20

The preserving formatting might be more complicated. I would encourage you to work out exactly what you'd like to see for various inputs. For example, exactly what should this look like after sorted?:

{:x 2 :c 2        :d 4
      :a 12345678 :y 1}
If you are not careful, you might end up scoping in some sort of vertical alignment feature.

lread14:11:36

But if you get too restrictive and only allow sorting on maps that look like:

{:z 2
 :a 1 
 :x 2
 :c 3}
Maybe a sort map feature is only marginally better than an editor/IDE's sort lines feature?

lread14:11:12

I guess I'm suggesting some hammock before coding.

ericdallo15:11:54

yeah, the idea is to add a code action on clojure-lsp that sort map keys, so I'd like to avoid changing formatting and things like that, only sort/swap the key-value pair with other key-value pair

ericdallo15:11:19

not sure it'd be a big issue if the result map become not correctly formatted, I think for now would be ok

ericdallo15:11:41

later then we could use cljfmt format after sorting when the vertical alignment is implemented :) (if user opt-in to that)

borkdude15:11:06

I have the same issue with rewrite-edn. it doesn't always do nice formatting, but at least all the original whitespace and comments are there

👍 1
borkdude15:11:27

what's the use case for sorting map keys? I've never had this desire

ericdallo15:11:04

https://github.com/clojure-lsp/clojure-lsp/issues/651 I do think it's something cool to have as a command, not priority tho

ericdallo15:11:32

I already saw that feature request on other libs like clj-refactor, and other editors

lread15:11:14

Well, I'm happy to fiddle up a little simplistic/naive example sometime later today for you.

❤️ 1
lread23:11:36

I’ve started to fiddle a bit late in the day… will continue tomorrow… Interesting… but… well.. we’ll see…

lread01:11:39

Hmm… I’ve got something kinda working… but there are many nuances to explore.

👀 1
lread16:11:23

So, the tricky part is not find the keys and values in the map. The tricky part, I think, is deciding what to do with spacing, comments and discards. Let’s start with basics, just in case there are lurkers who are interested. If we go all naive, and ignore the possibility of #_, here’s an example of finding all keys in a map:

(->> "{:a 1
       :b 2
       :c 3}"
     z/of-string
     z/down
     (iterate #(-> % z/right z/right))
     (take-while identity)
     (map z/string))
;; => (":a" ":b" ":c")
As you can see, rewrite-clj stores child nodes of a map in a simple sequence. It does not distinguish the key from the value. Here’s the same, but for values:
(->> "{:a 1
       :b 2
       :c 3}"
     z/of-string
     z/down
     z/right
     (iterate #(-> % z/right z/right))
     (take-while identity)
     (map z/string))
;; => ("1" "2" "3")

lread16:11:03

So it is not too terribly difficult to find keys and values, and one found, sort by the stringified key. But what what to do about: • spacing • comments • #_

lread16:11:24

For spacing, given kv-seq <k><spacing>+<v>, I am currently experimenting with moving kv-seq and leaving any surrounding whitespace where it is. This seems to work fine for simple cases:

{:c 3 
 :b 2 
 :a 1}
becomes, as you would expect:
{:a 1
 :b 2
 :c 3}
And with this strategy
{    :d 4
    :c 3
   :b 2
  :a 1 }
Becomes:
{    :a 1
    :b 2
   :c 3
  :d 4 }
But this currently can break down a bit if a kv-seq includes newlines. For example:
{:z 1 :b {:x 4
          :c 7}
 :a [1 2
     3 7]}
Becomes (you’ll notice that :c is no longer aligned under :x, and you will notice that nested maps are not automatically sorted):
{:a [1 2
     3 7] :b {:x 4
          :c 7}
 :z 1}
I suppose though, we could add in specific compensations for these cases. But first, I’d like your feedback on this general approach.

lread17:11:39

I’ve not looked a comments yet, but let’s explore a scenario:

{;; c1 is this a comment for the map or for the first kv?
 :z 1 ;; c2 this is most probably a comment for this kv and should move with it.
 ;; c3 is this a comment for a group of kvs or only the next kv, or is it a continuation of the comment for the last kv?
 :a 2
 :x 3}
I think if a comment trails a value on the same line, it is pretty clear it should be moved with the kv-seq. For the rest… well… not sure what we should do. Maybe just leave them where they are and let the user move them after the sort? Also notice that a trailing comment, if moved to the last element in the map, will mean moving the closing } , the above might become:
{;; c1 is this a comment for the map or for the first kv?
 :a 2
 ;; c3 is this a comment for a group of kvs or only the next kv, or is it a continuation of the comment for the last kv?
 :x 3
 :z 1 ;; c2 this is most probably a comment for this kv and should move with it.
}

lread17:11:56

For maps containing #_ and unbalanced maps, I think maybe our original idea of not sorting is good.

lread17:11:12

This is also an option for maps containing comments.

ericdallo17:11:18

Thank you for the help @UE21H2HHD! > Becomes (you’ll notice that `:c` is no longer aligned under `:x`, and you will notice that nested maps are not automatically sorted): I'm pretty ok on this minor alignment for now, users could just call clojure-lsp format after the sort or even clojure-lsp could do that automatically for that map when user triggers the sort :) > Maybe just leave them where they are and let the user move them after the sort? The same, I'm pretty ok on leaving comments not on the same line of a k-v not sorted

ericdallo17:11:55

I think most formatting cases we could fix with calling format for that map right after the sort.

lread17:11:19

Cool, I’ll explore moving the same-line comment, do a little cleanup and share what I’ve come up with.

gratitude 1
lread21:11:48

@ericdallo, the code was getting a bit long so I instead of a gist, http://nextjournal.com/lread/sorting-maps-by-key-with-rewrite-clj. Pretty cool! Lemme know watcha think and if this gets you on your way. If not, we can chat more.

ericdallo00:11:18

wow, what a detailed explanation @UE21H2HHD, that's awesome!

ericdallo00:11:06

thank you very much for the effort, also explaining it on nextjournal was very cool

lread00:11:38

Glad you are enjoying it! I'm not crazy about my excessive use of loop but not a bad first crack at it.

lread00:11:01

Yeah, nextjournal is pretty awesome.

ericdallo00:11:02

I understood the concepts of the function and I will even need to use the sortable-map? as well 😛

ericdallo00:11:38

I'll try to use on clojure-lsp and let you know if I find any issues, but it looks really good as a first iteration

lread00:11:40

Cool, you might want to add a few more tests as I only really touched the surface.

ericdallo00:11:07

Sure, I'll add tests on clojure-lsp side you should tweet about that article 🙂

lread00:11:36

Meh, I'm not much of a tweeter.

lread00:11:13

I'm in it for the fun, not the fame. 🙂

ericdallo00:11:55

haha, yeah I meant, people would love to see those explanations and code

lread00:11:55

Go ahead and tweet it if you like. I think I've only tweeted like 4 times. Don't want to get into the habit!

ericdallo00:11:49

haha, I will tweet later and tag you (if you don't mind)

ericdallo00:11:07

Also, I hope you don't mind if I co-author you the clojure-lsp commit

lread00:11:16

Ya, sure, sounds good.

👍 1
ericdallo01:11:53

@UE21H2HHD it works like a charm 🚀

nate04:11:40

Oh wow!! Thank you for implementing this so quickly!

👍 2
borkdude15:11:08

gratitude rewrite-clj is such a great project gratitude

lread15:11:11

Yeah, gotta agree, pragmatic and fun!

borkdude15:11:10

@ericdallo btw, Eric, I managed to do without making a change in rewrite-clj with regards to adding an id to the keyword nodes

ericdallo16:11:50

oh, that's nice!