parinfer

shaunlebron 2017-07-10T00:00:04.112051Z

my feeling is that this cursorDx solution is a partial one, but still useful enough for an early version, since it is not introducing more problems to Indent Mode, but reducing them

cfleming 2017-07-10T00:00:21.114501Z

Here’s a test case:

((if some-condition
   print
   println) {:foo 1
             :bar 2})

shaunlebron 2017-07-10T00:00:48.117704Z

and the smart mode can be a complete solution that takes an array of changes, but will require some work

cfleming 2017-07-10T00:01:07.119841Z

If you wrap the whole (if some-condition ... ), then the print and println should be indented by 1, and the map by 2.

shaunlebron 2017-07-10T00:02:01.125766Z

nice case

cfleming 2017-07-10T00:02:09.126516Z

It gets really hard with overlapping changes, too.

shaunlebron 2017-07-10T00:02:15.127182Z

do you control the wrap operation?

shaunlebron 2017-07-10T00:02:31.128850Z

could it replaced with a single change like atom does?

shaunlebron 2017-07-10T00:02:39.129659Z

(not paredit I mean)

cfleming 2017-07-10T00:03:16.133449Z

Well, I could do, but that then goes down the path of special-casing editing ops.

cfleming 2017-07-10T00:03:33.135201Z

It really needs a general solution to be robust.

cfleming 2017-07-10T00:03:49.136765Z

And I can’t special-case my way out of renaming, either.

shaunlebron 2017-07-10T00:04:25.140008Z

makes sense

cfleming 2017-07-10T00:04:43.141905Z

like:

(my-fn (if some-condition
         print
         println) my-fn {:foo 1
                         :bar 2})

cfleming 2017-07-10T00:05:06.144170Z

If I rename my-fn there, for example.

cfleming 2017-07-10T00:05:27.146238Z

(very contrived, of course, but I’m sure there are plenty of real cases).

shaunlebron 2017-07-10T00:07:27.157677Z

I think the changes option is doable for handling renames like this

shaunlebron 2017-07-10T00:07:40.158869Z

I track both input and output coordinates as I parse the text

shaunlebron 2017-07-10T00:08:00.160811Z

i’ll have to think more about it, but it feels doable

cfleming 2017-07-10T00:08:04.161193Z

Here’s how I think this might work:

cfleming 2017-07-10T00:09:04.167140Z

Really, the cursorDx at the moment is “from some point in the file (the new end offset of a change), indents must be changed by dx until a close paren”

cfleming 2017-07-10T00:09:22.168887Z

Is that correct?

shaunlebron 2017-07-10T00:09:38.170324Z

yes

cfleming 2017-07-10T00:10:33.175819Z

So it seems like you could accept a list of those in result-document-order, and essentially keep a stack of them - they’re scoped by the parens, really.

cfleming 2017-07-10T00:11:02.179074Z

When you encounter a new one, it gets pushed on the stack, and when you reach the corresponding close paren, you pop it.

cfleming 2017-07-10T00:11:28.181758Z

The total dx at any point in time is the sum of the dxes on the stack.

cfleming 2017-07-10T00:13:04.191632Z

At least in IntelliJ, calculating that list is relatively easy.

cfleming 2017-07-10T00:14:16.198699Z

I think that’s right, and it actually should be relatively simple to implement.

cfleming 2017-07-10T00:14:47.201763Z

It works nicely with the way parinfer works right now.

shaunlebron 2017-07-10T00:15:41.207268Z

“result-document-order”?

cfleming 2017-07-10T00:16:26.211698Z

Really, a cursorDx is the dx value and the offset in the doc after which to apply it.

cfleming 2017-07-10T00:16:42.213332Z

So just ordered by the offsets in the document.

cfleming 2017-07-10T00:17:29.217971Z

Whoever gives them to you will have to take care that they’re in the correct order, and also that they’re updated to use the co-ordinates in the resulting document.

shaunlebron 2017-07-10T00:17:37.218806Z

sounds good

shaunlebron 2017-07-10T00:18:01.221209Z

what would you call this option? it should replace cursorDx

cfleming 2017-07-10T00:18:09.221922Z

changeDx

cfleming 2017-07-10T00:18:19.223051Z

Or editDx, perhaps.

cfleming 2017-07-10T00:18:53.226510Z

It’s the change in the indentation caused by a particular change or edit.

cfleming 2017-07-10T00:19:29.230295Z

But perhaps just call it changes? It will be a list of [offset dx] pairs.

shaunlebron 2017-07-10T00:20:05.233825Z

maybe I should just take the changes and compute the [offset dx] internally

cfleming 2017-07-10T00:20:14.234835Z

I suspect there are hairy edge cases here if the changes are not balanced, thinking about it.

shaunlebron 2017-07-10T00:20:15.235072Z

and then we can use that option for other things later

cfleming 2017-07-10T00:20:37.237571Z

Imagine that a user selects some text including an open paren, and then replaces it with text containing no parens.

shaunlebron 2017-07-10T00:21:22.242570Z

what problem would that cause?

cfleming 2017-07-10T00:27:40.280475Z

Because these will be applied using the parens for scoping. Let me see if I can think of a case.

cfleming 2017-07-10T00:28:40.286807Z

(test {:foo 1
       :bar 2})

cfleming 2017-07-10T00:29:01.289065Z

Imagine that the user selects test { and replaces it with foo.

cfleming 2017-07-10T00:29:30.292292Z

I guess that would work, actually, since the closing } will be removed during processing anyway

cfleming 2017-07-10T00:30:25.298614Z

I wonder if there are tricky cases when the user deletes closing parens.

cfleming 2017-07-10T00:30:44.300638Z

Anyway, I guess we’ll find them soon enough.

shaunlebron 2017-07-10T00:31:57.308564Z

yeah, I couldn’t think of a problem with it actually, other than causing extra character insertion that could throw off the other change coordinates if I’m doing it incorrectly

shaunlebron 2017-07-10T00:39:25.356019Z

will look at this after dinner, thanks so much guys. couldn’t have done this myself

cfleming 2017-07-10T00:40:32.363048Z

Thanks to you too - I’m really excited for this change!

cfleming 2017-07-10T00:41:57.372180Z

Actually….

cfleming 2017-07-10T00:42:20.374624Z

Even more exciting, I think this can be used for the partial update change.

cfleming 2017-07-10T00:43:31.382265Z

As you’re processing, you record more or less the same data you’re doing now, but don’t make any modifications.

cfleming 2017-07-10T00:44:20.387557Z

When you encounter a change, you know when the previous open paren was. You then process, making modifications, until that paren is balanced again.

cfleming 2017-07-10T00:44:23.387789Z

OMG

cfleming 2017-07-10T00:45:11.393078Z

Then continue without making modifications until you reach another change.

cfleming 2017-07-10T00:46:19.400462Z

In fact, it’s possible you could have two modes - scanning and only checking that parens are balanced, and then a mode where you’re processing, basically exactly as you do now. You just need to set up the correct data at the start of the open paren previous to the change.

shaunlebron 2017-07-10T01:13:08.598055Z

ha, not following yet

cfleming 2017-07-10T01:42:31.835958Z

I’m looking after our daughter now, I’ll try to explain better later.

cfleming 2017-07-10T01:42:51.838548Z

But I think this might be the change which achieves everything I wanted from parinfer 🙂

shaunlebron 2017-07-10T02:50:01.435941Z

streaming some work tonight: https://www.twitch.tv/shaunlebron

cfleming 2017-07-10T03:51:12.960134Z

@shaunlebron So I basically wanted to change two things from parinfer, which I was planning to investigate. One was removing the indent/paren mode difference. The other was only processing the parts of the file (sexps, probably) affected by a particular change. This would mean that people wouldn’t have to pre-format their files on file open, and it would not be a requirement that the whole file complied with the indent rules - just the parts that they’re editing. My idea was to detect while processing the affected part of the file if that particular section didn’t comply with the indent rules, and to show an error at that point. So if the user tried an action in a part of the file which couldn’t support indent mode, parinfer just wouldn’t get run, they would be notified and they would have to fix it by hand, reformat it or something similar.

cfleming 2017-07-10T03:51:37.963449Z

This change you’re making fixes the first, and I believe it can also be made to do the second.

cfleming 2017-07-10T03:53:46.981386Z

I think the best way to do this would be to start scanning the file in a non-modifying mode. This would scan the file contents much like parinfer does at the moment, but it would not actually modify the file, it would just record the paren stack, the current horizontal position and the position of the last open paren (that might be implicitly stored in the paren stack).

cfleming 2017-07-10T03:57:29.012636Z

Then, when you encounter one of these changes, you switch to processing exactly like parinfer does now, or at least like your new modified version does. You record the last open paren you saw. You update the file, but critically you stop processing as soon as the last open paren you recorded earlier is balanced. This means that the paren trail handling is a little trickier since you don’t just delete all close parens wholesale, you only do it until they’re balanced. Once your original open paren is balanced again, you switch back to your non-modifying scanning mode again until you hit another change.

cfleming 2017-07-10T04:00:28.039895Z

If you ever get to a stage where the parens can’t balance, you barf with an error - this is like your new v2 warning mode. Similarly, you barf with an error if the sections you’re actually processing don’t obey the indent mode rules - I’m not sure if this requires more information to be tracked than is currently, but I suspect it’s a relatively simple change.

cfleming 2017-07-10T04:03:00.063580Z

Obviously I haven’t implemented this, but in my head it works perfectly 🙂

cfleming 2017-07-10T04:05:09.082094Z

The only bit that I don’t have quite clear in my head (been too long since I looked at all the details) is that the close paren tracking & matching will work correctly - I’d need to try it.

cfleming 2017-07-10T04:57:35.517538Z

Actually, thinking about this, I’m not sure it will work - I don’t know if parinfer can work out when processing should stop, since normally it relies on removing all close parens and putting them back where it infers them.

cfleming 2017-07-10T04:58:17.523513Z

I think it will probably need the original range of the sexp from the original document, which gets back to the complexity around tracking ranges across all the changes.

shaunlebron 2017-07-10T17:15:06.920025Z

just thinking through an example:

(defn foo
  ([a]
     (foo a 123)) ;; <--- what happens when adjusting indentation of this line?
  ([a b]
     (+ a b)))

shaunlebron 2017-07-10T17:16:39.970939Z

having a hard time piecing together expected behavior here, maybe we can work from this example to see what would be partially processed

shaunlebron 2017-07-10T17:31:50.479642Z

some questions: 1. should dedenting this line by one space cause it to enter the parent form? 2. should dedenting all the way cause it to adopt everything below it?

shaunlebron 2017-07-10T17:44:01.882547Z

@rgdelato: I published 2.5.2

shaunlebron 2017-07-10T17:44:30.898877Z

you can either run with that, or wait for a cursorDx to be replaced with a changes array option

shaunlebron 2017-07-10T17:45:16.925321Z

i’m leaving for a road trip tomorrow, and I may not finish the changes thing today

rgdelato 2017-07-10T17:46:21.961796Z

cool, have fun on your trip!

👍 1
cfleming 2017-07-10T18:14:49.934429Z

@shaunlebron > maybe I should just take the changes and compute the [offset dx] internally I think this would be a good idea BTW, then you have more context if you want it later. It means explaining the concept of the different co-ordinate spaces and specifying which you want though.

cfleming 2017-07-10T18:15:04.943070Z

More thoughts later

shaunlebron 2017-07-10T19:19:47.039022Z

thanks colin!

shaunlebron 2017-07-10T19:20:08.049103Z

streaming here for the changes option: http://twitch.tv/shaunlebron

cfleming 2017-07-10T22:29:08.215482Z

@shaunlebron So that’s a tricky case to start with 🙂

cfleming 2017-07-10T22:30:41.244129Z

One thing I’m not clear on is if when you encounter a change, if you need to reprocess from the last opening paren. I’m not sure how much data you would need to collect to begin processing immediately when encountering a change.

cfleming 2017-07-10T22:32:43.281726Z

So I think assuming that either you reprocess from the opening paren, or you do have enough data from the initial scanning, when adjusting the indentation of that line the last opening paren will be the ([a]...

cfleming 2017-07-10T22:32:54.284935Z

i.e. we’ll try to process the whole arity 1 body.

cfleming 2017-07-10T22:33:31.296242Z

But as soon as we start processing it, we’ll find that it’s not indented according to the indent mode rules, and fail with an error stating that.

cfleming 2017-07-10T22:33:47.300803Z

Let’s start with an example that actually works first.

cfleming 2017-07-10T22:36:05.341502Z

(let [{:keys [refer rename]} filters
      name (get rename place-name place-name)]
  (when (or (= :all refer) ; <-- replace this when with an if
            (contains? refer name))
    name))

cfleming 2017-07-10T22:37:23.364924Z

After replacing:

cfleming 2017-07-10T22:37:49.372351Z

(let [{:keys [refer rename]} filters
      name (get rename place-name place-name)]
  (if (or (= :all refer) ; <-- change is right after if, dx = -2
            (contains? refer name))
    name))

cfleming 2017-07-10T22:38:42.387322Z

Now, when you run parinfer, you’ll scan the file, not modifying anything but just checking parens are balanced, until you hit the change.

cfleming 2017-07-10T22:40:28.417056Z

So your last open paren is right before the if, and you start processing. I’m assuming that at that point you’re processing in paren mode, because you have an active dx.

cfleming 2017-07-10T22:42:29.452224Z

I haven’t seen that code, so I don’t know exactly how that works, but I’m assuming that dx is in scope for the following block, i.e. the (or ...) sexp, so that all gets indented correctly:

cfleming 2017-07-10T22:42:53.459175Z

(let [{:keys [refer rename]} filters
      name (get rename place-name place-name)]
  (if (or (= :all refer)
          (contains? refer name)) ; <- this gets indented by dx
    name))

cfleming 2017-07-10T22:44:18.483753Z

then you continue processing until the end of name). At that point you realise that your original open paren (if... is now balanced, so you go back to scanning. Since there are no more changes, you’re done.

cfleming 2017-07-10T22:45:07.497989Z

So you only made edits inside the if sexp, and the forms in the let binding vector could have been incorrectly indented (according to indent mode rules), and that wouldn’t have been a problem as long as the parens are correctly balanced.

cfleming 2017-07-10T22:45:38.506870Z

If something inside the if block were incorrectly indented, you’d fail with an error.

cfleming 2017-07-10T22:48:45.560090Z

Here’s another case:

cfleming 2017-07-10T22:49:28.571813Z

(let [{:keys [refer rename]} filters
      name (get rename place-name place-name)]
  (if (or (= :all refer) ; <- caret is now right before (or…, and I type or paste (and 
          (contains? refer name))
    name))

cfleming 2017-07-10T22:49:50.578024Z

Because I’m going to make my conditional more complicated.

cfleming 2017-07-10T22:50:41.592155Z

So now we have:

(let [{:keys [refer rename]} filters
      name (get rename place-name place-name)]
  (if (and (or (= :all refer) ; <- and is currently unbalanced, following code is not indented
          (contains? refer name))
    name))

cfleming 2017-07-10T22:51:28.605577Z

Again, assuming that dx is in scope for the or block, you process that in paren mode:

cfleming 2017-07-10T22:51:53.612272Z

(let [{:keys [refer rename]} filters
      name (get rename place-name place-name)]
  (if (and (or (= :all refer) ; <- and is still unbalanced
               (contains? refer name))
    name))

cfleming 2017-07-10T22:52:52.628495Z

Then at the end of the (or...) block, you’re processing in indent mode again as normal, and add the closing paren for the (and...

cfleming 2017-07-10T22:53:07.632784Z

(let [{:keys [refer rename]} filters
      name (get rename place-name place-name)]
  (if (and (or (= :all refer)
               (contains? refer name)))
    name))

cfleming 2017-07-10T22:53:34.640177Z

Then as before, you keep processing until the end of name), switch to scanning and run to the end.

cfleming 2017-07-10T22:54:01.647667Z

The tricky part here (and in the previous case) is how to handle the paren trail for name))

cfleming 2017-07-10T23:01:48.780701Z

Actually, looking back at the existing paren trail rules, I’m not sure they need to be any different.

cfleming 2017-07-10T23:02:40.795793Z

The one tricky bit might be: IIRC at the end of a file, if there are any remaining outstanding close parens they’re inserted at the end to ensure everything is balanced.

cfleming 2017-07-10T23:03:21.807434Z

Should that happen here when the open paren before the change is balanced? I guess by definition there can be no outstanding close parens at that point.

shaunlebron 2017-07-10T23:37:20.323078Z

2.6.0 pushed with changes option that replaces cursorDx

shaunlebron 2017-07-10T23:38:00.332772Z

@rgdelato @chrisoakman we can start integrating the changes option into atom-parinfer now

shaunlebron 2017-07-10T23:38:16.336901Z

documented here: https://github.com/shaunlebron/parinfer/tree/master/lib#api

shaunlebron 2017-07-10T23:39:02.348206Z

I’ll try to update the site demo tonight if I have time 🙂

cfleming 2017-07-10T23:39:12.350621Z

don’t sweat it.

cfleming 2017-07-10T23:39:34.356074Z

Assuming this looks good in Atom I’ll port to Kotlin and try it in IntelliJ.

cfleming 2017-07-10T23:39:44.358375Z

And I’ll experiment with the partial change thing too.

cfleming 2017-07-10T23:39:46.359066Z

Thanks for this!