jwoudenberg / elm-pair

An artificial pair-programmer that helps you write Elm

Home Page:https://elm-pair.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

elm-pair interacts with Vim undo history in a weird way

Janiczek opened this issue · comments

elm-pair seems to interact with the Vim history in a weird way (but only in some refactors): instead of there being a linear history, by repeatedly pressing u I essentially end up cycling between two states now and I never get to any "first" state. I reckon that's because as I undo, elm-pair runs again, making changes and creating a new undo point.

Steps to reproduce

elm.json:

{
    "type": "application",
    "source-directories": [
        "."
    ],
    "elm-version": "0.19.1",
    "dependencies": {
        "direct": {
            "elm/browser": "1.0.2",
            "elm/core": "1.0.5",
            "elm/html": "1.0.0"
        },
        "indirect": {
            "elm/json": "1.1.3",
            "elm/time": "1.0.0",
            "elm/url": "1.0.0",
            "elm/virtual-dom": "1.0.2"
        }
    },
    "test-dependencies": {
        "direct": {},
        "indirect": {}
    }
}

Main.elm:

module Main exposing (main)

import Foo exposing (Foo)


run : Foo -> ()
run foo =
    ()


main : Program () () ()
main =
    Platform.worker
        { init = \_ -> ( (), Cmd.none )
        , update = \_ _ -> ( (), Cmd.none )
        , subscriptions = \_ -> Sub.none
        }
module Foo exposing (Foo)


type Foo
    = X
    | Y
  1. Open the file Main.elm
  2. Move cursor to the space after import Foo
  3. Press D to make this edit:
-import Foo exposing (Foo)
+import Foo

Now, when you repeatedly press u, you'll create more and more undo blocks instead of getting to the beginning.

Recording

vid.mp4

Wild guesses at how to fix this

First of all, I don't understand why this particular refactor makes Vim go crazy and the others do not. Maybe there's something about how the refactor commands are implemented, I don't know.

That disclaimer out of the way, I'm not a Vim expert, and this might need some careful :help undojoin reading, but! I have this snippet for elm-format lying around in my ~/.vimrc that uses undojoin to (?) merge the automated format-on-save change with whatever I did before... so I think it might help in this elm-pair issue too.

augroup fmt
  autocmd!
  autocmd BufWritePre *.elm try | undojoin | Neoformat | catch /^Vim\%((\a\+)\)\=:E790/ | endtry
  "                              \--------------------/
  "                                 ^ this is the interesting part
augroup END

From cursory reading of the elm-pair Vim plugin I don't really understand how the elm-pair binary gets notified about the specific edits (there's no augroup, so perhaps this is handled by the RPC machinery?), so I don't feel like I know how to test the undojoin idea out. You'll probably know more. Hopefully it helps.

Thank you for the report, it's super comprehensive as usual. I'll look into this!

I reckon that's because as I undo, elm-pair runs again, making changes and creating a new undo point.

Yeah, I think this is right.

From cursory reading of the elm-pair Vim plugin I don't really understand how the elm-pair binary gets notified about the specific edits (there's no augroup, so perhaps this is handled by the RPC machinery?)

Yeah, the plugin sets things up so the elm-pair process can send Vim commands over RPC. Then the elm-pair sends over the RPC channel a command to subscribe to events in Elm buffers, here.

Undojoin looks interesting and I think I understand how it might help with this problem, but I'm not sure I want to fuse the programmer action and elm-pair reaction together into a single undo point. As a fail-safe for when Elm-pair guesses intent wrong (rarely, if it works well!) I want undo to undo just the changes Elm-pair made. If your own changes are undone as well, then reintroducing them after the undo is going to trigger Elm-pair to make the same mistake again.

Thinking out loud: perhaps elm-pair could somehow figure out which edits are "normal" and which ones are undos, and not run at all on the undo changes?

There still is the question of why some other elm-pair refactors don't have the infinite undo issue. I guess the inverse change (done by the undo) doesn't trigger elm-pair? So the infinite undo situation only happens when both the user change and its inverse (undo) trigger elm-pair to make changes to the buffer?

Thinking out loud: perhaps elm-pair could somehow figure out which edits are "normal" and which ones are undos, and not run at all on the undo changes?

Yeah, I think that would be a good solution. I don't believe that information is attached to the change events I'm getting from Neovim. Maybe I can get Elm-pair to cheaply detect that a change is an exact inverse of an earlier change. Then future editor integrations would be less work (because we wouldn't need to replicate the 'is this an undo'-operation for each separately).

There still is the question of why some other elm-pair refactors don't have the infinite undo issue. I guess the inverse change (done by the undo) doesn't trigger elm-pair? So the infinite undo situation only happens when both the user change and its inverse (undo) trigger elm-pair to make changes to the buffer?

This is my guess. Elm-pair only recognizes single changes to the code. That's why it needs to listen to many tiny events as you type, it might 'miss' its window otherwise as you finish typing one logical change and start with the next. Many refactors result in Elm-pair changing code in multiple places. Undoing such a refactor then also changes code in multiple places and Elm-pair won't have a reponse to that. Your example is very neat and tidy: change code in one place, Elm-pair changes code in one other place.

This is unexpectedly tricky!

My first thought was to keep track of the last refactor, so when a change happens we can compare it with the last refactor and if they're inverses, we have an undo! I got this to work, but then realized this only helps you through a single undo, the most recent one. Unless Elm-pair starts tracking the entire history of the buffer (really don't want to go there) I don't think it's possible to do better with this approach. Maybe it's better than nothing, but I'm aiming higher than that.

My second thought was to start feeding undo-events to Elm-pair. In reponse to one Elm-pair could bust the 'last compiling version' it has on record for the same buffer, meaning it wouldn't make any changes until the programmer gets the code to a compiling state again by hand. One problem with this is that I can't find a way to subscribe to undo events in (Neo)vim. Another is that this approach would mean Elm-pair goes into hands-off mode regardless of whether the programmer is undoing a change they made themselves or one made by Elm-pair. In practice I think this would mean that Elm-pair would not work at seemingly arbitrary moments (I don't know about you, but I'm not super conscious about hitting undo).

My third throught is to 'poison' all refactors in a way that makes their reverts easily recognizable and/or not hit any of the patterns that would trigger a new refactor. The only refactors at risk of getting us into an undo-loop are the ones that change a single slice of code into something else. Maybe Elm-pair could add a second change to these, one that's not visible to the programmer ideally or maybe elm-format clears away automatically. Don't know what that change would be though. I'll think a bit about it. If you've any ideas let me know!

After thinking about it more, I think the first solution performs reasonable in a larger undo operation too. If you make a change X that results in an Elm-pair refactor, then a bunch of other changes, and then undo the lot, elm-pair might try a refactor when you undo over X once. But undoing that again does allow you to move past it. I think that's reasonable (though better approaches would still be welcome). I've tested it a bit and it seems to behave. I'll push it, cutting a release, and then I'd love to hear what you think!

Release 9 is online!

Yup it works great! As you say, there's one extra step, but still, a massive improvement. Thanks for fixing this!

Thank you for bringing this, and talking through solutions with me! Going to close this issue given I've no ideas for improving this further at present.