kkinnear / zprint

Executables, uberjar, and library to beautifully format Clojure and Clojurescript source code and s-expressions.
MIT License
547 stars 46 forks source link

Request or help if exists already - `#js` reader syntax in cljs interop #292

Closed riotrah closed 10 months ago

riotrah commented 1 year ago

I write a lot of cljs, and thus a lot of js interop. This frequently involves use of #js {:my-key ""} and #js [""]

I'm not sure what it is about my config, which is otherwise really great, but whenever a hang or flow are warranted (line too long, or explicitly configured, etc), the #js ends up on its own line.

Is that expected behavior? Is there, or could there be, a config option to keep #js always adjacent (left of) the subsequent form, whilst maintaining the rest of the hang/flow?

kkinnear commented 1 year ago

I think I'm going to need your config and an actual example to make progress on this. When I try it with the default config, it does exactly what you want:

(czprint i292 {:parse-string? true :width 50})
(defn i292
  [x y]
  (let [this-is (a-test this
                        is
                        only
                        a
                        test
                        #js {:my-key ""})]
    (vector? this-is)))

I checked, and it has worked like this for a good long time in the past.

Something in your configuration must be triggering the problem, so if you give me an example and your config, I'll figure out what it is and see what I can to do to prevent it. Thanks!

riotrah commented 1 year ago

Oh interesting. Thanks for the promptness and intense dedication as always, @kkinnear.

Apologies for the verbose configs (namely the fn-maps, which I'm probably doing innefficiently and redundantly across my user & repo configs)

Here's my _user_ config: ```clojure {:fn-map {":require" [:flow {:list {:indent 2 :indent-arg 2}}] "$" [:gt2-force-nl {:list {:hang? true} :map {:force-nl? true}}] "$r" [:gt2-force-nl {:list {:hang? true} :map {:force-nl? true}}] "->" [:arg1-force-nl {:list {:hang? true}}] "->>" [:arg1-force-nl {:list {:hang? true}}] ".then" [:arg1-force-nl {:list {:hang? true}}] "<>" [:arg1-force-nl {:list {:hang? true} :map {:force-nl? true}}] "cond" :pair-fn ;; "cond->" [:pair {:list {:hang? true}}] "cond->>" [:arg1-force-nl {:list {:hang? true}}] "css" :arg1-force-nl "cx" :arg1-force-nl "def" [:arg1-force-nl {:list {:hang? false}}] "defn" [:arg1-force-nl {:list {:hang? false}}] "defnc" :arg2 "fnc" :arg1 "defhook" :arg1-force-nl "use-effect" :flow "defstory" :arg1-force-nl "deftest" [:arg1-force-nl {:list {:hang? false :respect-bl? true}}] "deftest-chain" [:arg1-force-nl {:list {:hang? false}}] ;; "fn" :gt2-force-nl "fn*" [:replace-w-string {:list {:replacement-string "#"}}] ;; "if" :arg1-force-nl "if" :hang "into" :arg1-force-nl ;; "is" [:arg1 {:list {:hang? true}}] "js-await" [:binding {:list {:hang? false}}] "merge" :arg1-force-nl "quote" [:replace-w-string {:list {:replacement-string "'"}}] "reg-event-db" :flow "reg-event-fx" :flow "testing" [:arg1-force-nl {:list {:hang? false}}] "use-*" :arg1 "when" :arg1-force-nl "when-not" :arg1-force-nl} :list {:indent-arg 2} :map {:comma? false :force-nl? true :justify? true} :pair {:hang? true :justify? false} :search-config? true :style [:community :hiccup :how-to-ns] :vector {:wrap-after-multi? true :wrap? true} :width 120} ```
Here's my _repo_ config: ```clojure {:binding {:flow? false :force-nl? true :hang-diff 10 :hang-expand 5000 :hang? true :indent 2 :justify {:ignore-for-variance nil ;; :lhs-narrow 2.0 :max-variance 1000 :no-justify #{"_"}} :justify? false :nl-separator? true} :comment {:inline-align-style :consecutive :inline? true :wrap? true} :fn-map {"$" [:gt2-force-nl {:list {:hang? true} :map {:force-nl? true}}] "$r" [:gt2-force-nl {:list {:hang? true} :map {:force-nl? true}}] "->" :hang ;; "->" [:arg1-force-nl {:list {:hang? true ;; :indent 4 ;; :indent-arg 4}}] "->>" [:arg1-force-nl {:list {:hang? true}}] ".then" [:arg1-force-nl {:list {:hang? true}}] ;; :default :arg1 :default :hang "concat" :hang "gobj/set" [:arg1 {:list {:hang? true :indent-arg 10}}] ":require" [:flow {:list {:indent 2 :indent-arg 2}}] ":require-macros" :force-nl "<>" [:arg1-force-nl {:list {:hang? true} :map {:force-nl? true}}] "assoc" [:arg1-pair {:pair-fn {:hang? true} :pair {:hang? true :flow? false}}] "cond" :pair-fn "cond->>" [:arg1-> {:list {:hang? true}}] "css" [:arg1-force-nl {:list {:hang? true}}] "d/div" :flow "def" [:arg1-force-nl {:list {:hang? false}}] "defcss" [:arg1-force-nl {:list {:hang? false}}] "defn" [:arg1-force-nl {:list {:hang? false}}] "defnc" [:arg1-force-nl {:list {:hang? false}}] "defnc-" [:arg1-force-nl {:list {:hang? false}}] "defstory" [:arg1-force-nl {:list {:hang? true}}] "deftest" [:arg1-force-nl {:list {:hang? false :respect-bl? true} :next-inner-restore [[:list :hang?] [:list :respect-bl?]]}] "deftest-chain" [:arg1-force-nl {:list {:hang? false} :next-inner-restore [[:list :hang?] [:list :respect-bl?]]}] "fn" :fn "fn*" [:replace-w-string {:list {:replacement-string "#"}}] "for-map" :binding "if" [:hang {:list {:hang? true}}] "into" [:arg1-force-nl {:list {:hang? true}}] "is" [:arg1 {:list {:hang? true}}] "js-await" [:binding {:list {:hang? false}}] "let" :binding "merge" :hang "ns" [:arg1-force-nl {:list {:hang? false}}] "quote" [:replace-w-string {:list {:replacement-string "'"}}] "reg-event-db" :flow "reg-event-fx" :flow "testing" [:arg1-force-nl {:list {:hang? false} :next-inner-restore [[:list :hang?] [:list :respect-bl?]]}] "when" [:arg1-force-nl {:list {:hang? true}}] "when-not" [:arg1-force-nl {:list {:hang? true}}]} :list {:hang? true} :map {:comma? false :force-nl? true :hang? true :justify? false} ;; :pair {:hang? true} ;; :pair-fn {:hang? true} :search-config? true ;; :width 80} :width 100} ;; :width 120} ```
kkinnear commented 1 year ago

Thanks for the configurations. That didn't provoke the problem, interestingly enough. It seems like, using the default configuration, that you get the #js on a line by itself when the map after it doesn't fit on the same line. Like this:

(defn i292
  [x y]
  (let [this-is (a-test this
                        #js
                         {:your-key "",
                          :a-very-long-key :and-a-very-long-value,
                          :a-second-key :and-a-second-value}
                        a
                        test
                        #js {:my-key ""}
                        not
                        the
                        end)]
    (vector? this-is)))

Notice the two #js elements above.

Questions:

  1. I think the first #js is what you don't like and the second is what you do like. Is this correct?
  2. Is this the behavior that you see? That is, are the places where #js appears on its own line places where the thing following it would not fit on the same line as the #js?
  3. Do you ever see things like the second #js, where the next thing stays on the same line as the #js?

Thanks!

riotrah commented 1 year ago
  1. Yes, tho needs some upcoming context
  2. Yep
  3. Yep! I neglected to even notice that as all of my usages of that recently have been for somewhat larger/nested pojos/maps

Your first example comes close, tho it's a more "justifiable" (?) case for a separate line #js , imo.

But here's a real-er case:

suppose there's a max width set somewhere near the end of the longest line

Before:

                   #js {:a "b"}
                   #js {:detail (or signature (str "[" (name (or category "")) "]"))
                        :documentation (when description
                                         #js {:value description})
                        :insertText (if function? (str/upper-case insert-text) insert-text)
                        :insertTextRules (when snippet
                                           insert-as-snippet-rule)
                        :kind kind
                        :label s
                        :filterText (str/upper-case s)
                        :sortText (str/lower-case (str sort-text-prefix " " s))}))

After zprint:

                     #js {:a "b"}
                     #js
                      {:detail (or signature (str "[" (name (or category "")) "]"))
                       :documentation (when description
                                        #js {:value description})
                       :insertText (if function? (str/upper-case insert-text) insert-text)
                       :insertTextRules (when snippet
                                          insert-as-snippet-rule)
                       :kind kind
                       :label s
                       :filterText (str/upper-case s)
                       :sortText (str/lower-case (str sort-text-prefix " " s))}))

I'm not sure how I'd configure zprint to do what my preferred output would look like:

Preferred:

                   #js {:a 'b}
                   #js {:detail (or signature 
                                    (str "[" (name (or category "")) "]"))
                        :documentation (when description
                                         #js {:value description})
                        :insertText (if function? 
                                        (str/upper-case insert-text) 
                                        insert-text)
                        :insertTextRules (when snippet
                                           insert-as-snippet-rule)
                        :kind kind
                        :label s
                        :filterText (str/upper-case s)
                        :sortText (str/lower-case (str sort-text-prefix 
                                                       " " s))}

The specifics of where exactly all of the newlines should be or when to hang/flow/etc are less relevant (for this demo at least) than the main point being to start "breaking" the lines in the map before considering placing the #js on its own line. Ie, I'd rather have a map with multiline rhs values before I'd have a map whose #js is a line above it.

Does that make sense?

kkinnear commented 1 year ago

Yes, it makes a lot of sense.

It is worth knowing that you can't configure zprint to do it the way you want. At least I haven't found any way to do that. I am experimenting with a code change that will do it the way you want, which I think is probably what most people would want too. I'm currently looking at how to get it to go back to the not-great way it does it now in case someone really wants that.

riotrah commented 1 year ago

Wow thanks so much for exploring this wow!!!!!!

kkinnear commented 1 year ago

In 1.2.6 the #js will try to put the next thing on the same line, as you want it, by default. There will be a new configuration element in the option map: :tagged-literal which will be configurable in such a way that if someone wants it the way it is today, they can configure it that way by setting{:tagged-literal {:hang? false}}. I'll let you know when 1.2.6 is available.

riotrah commented 1 year ago

wow thanks again dude!

kkinnear commented 1 year ago

I just released 1.2.6. The changes I made to format tagged literals are the default, so you should see what you want without anything other than upgrading. Thanks for pointing this out!