ggandor / leap.nvim

Neovim's answer to the mouse 🦘
MIT License
4.35k stars 46 forks source link

Dot-repeat: custom implementation, vendor repeat.vim? #50

Open bjornevik opened 2 years ago

bjornevik commented 2 years ago

Nvim version: NVIM v0.8.0-dev-1132-g37a71d1f2

init.lua

require("leap").set_default_keymaps()

Use nvim -u init.lua to load minimal config

Do a movement like dz12 or cz12 and try to dot-repeat it. Looks like the editor is locked into a sort of operator-pending mode to my eyes.

Having the same problem with flit.nvim and lightspeed.nvim. First noticed it using f/t motions with lightspeed.

ggandor commented 2 years ago

Does this problem occur after undoing a dot-repeat? That is a known Vim issue, according to vim-sneak's docs: https://github.com/justinmk/vim-sneak/blob/94c2de47ab301d476a2baec9ffda07367046bec9/doc/sneak.txt#L200-L202

However, except for the above scenario, dot-repeat should work fine.

bjornevik commented 2 years ago

Does this problem occur after undoing a dot-repeat?

If I run through nvim --clean I'm able to dta -> dot-repeat -> undo -> dot-repeat as expected. If I attempt to do the same with leap/flit enabled I only get as far as dta -> dot-repeat, before nvim seizes up. If that's what you mean.

Might still be related to the issue referred to in vim-sneak docs, tried completely uninstall nvim-surround since I thought it might intefere, but still having this problem with nvim-surround disabled.

edit: looks like the dot-repeat enters d~@ý, at least it's displayed in the bottom right.

ggandor commented 2 years ago

looks like the dot-repeat enters d~@ý, at least it's displayed in the bottom right.

Yep. It is also interesting that this only happens after one dot-repeat - that is, dot-repeat -> dot-repeat+ -> undo works). Note that you can exit from this frozen state with C-c, if necessary.

ggandor commented 2 years ago

I only get as far as dta -> dot-repeat, before nvim seizes up

To be clear: you experience this problem without undoing the change first?

bjornevik commented 2 years ago

Yep. It is also interesting that this only happens after one dot-repeat - that is, dot-repeat -> dot-repeat+ -> undo works).

That isn't the case for me, I'm stuck in the state until I either kill neovim, or C-c.

To be clear: you experience this problem without undoing the change first?

Yes. On both MacOS and Ubuntu.

bjornevik commented 2 years ago

Tested some more today, seems I did not read the README properly and never added the tpope/vim-repeat dependency 🙈

It works now, though dot-repeat -> undo -> dot-repeat still has the issue. I'll leave it up to you whether or not to close this issue.

bjornevik commented 2 years ago

Have you considered changing the dot-repeat implementation as described in this gist? Was thinking of trying to implement it and opening a PR, but am not really familiar with fennel. Also don't want to repeat work if you've already considered it and decided against it.

ggandor commented 2 years ago

Tested some more today, seems I did not read the README properly and never added the tpope/vim-repeat dependency

Well... :D

Have you considered changing the dot-repeat implementation as described in this gist? Was thinking of trying to implement it and opening a PR, but am not really familiar with fennel. Also don't want to repeat work if you've already considered it and decided against it.

A PR is absolutely welcome, if the result is not significantly more verbose than the current implementation with vim-repeat. I've seen that gist, but haven't looked into it deeply yet, still don't really understand what we would need to do.

gegoune commented 2 years ago

gitsigns recently implemented (not sure if based on that gist) its own support for dot-repeat (without pope's plugin) as well. Just FYI.

bjornevik commented 1 year ago

Looked into it a couple of afternoons this past week, but haven't had the time to write any code.

A lot of it became clearer when I found this fantastic blog post by numToStr

How I've understood it is that the general pattern is to have a function callback that sets vim.o.operatorfunc to itself and returns g@ when it is called "without a motion" (aka. called by user), and otherwise does whichever action when it is called with a motion. When g@ is called the user is popped into operator-pending mode, and when the user enters the motion g@ will call whatever function is set in vim.o.operatorfunc with the motion as an argument. Dot-repeat is automatically then set to g@[motion] with the same motion the user did and will repeat as expected. You can also set vim.o.operatorfunc

Which should be relatively straight forward, but my non-existent experience with fennel (and lisp in general) is making it a bit harder to read the code and figure out what leap actually does and how everything fits together. Unsure if it's just a matter of changing a couple of lines in fn set-dot-repeat* [] or if there needs to be a separate callback function for handling the different operators, visual mode, etc. Or even if it would constitute a significantly more verbose solution.

FelipeLema commented 1 year ago

changing to operatorfunc instead of depending repeat.vim is hard because the model for using operatorfunc needs to save a state needed to repeat the motion outside the command doing the motion.

Alternatively, one can try to re-use the current model (handling registers and operators ourselves), but that would involve handling corner cases that repeat.vim already handles (would require re-writing lots of code).

ggandor commented 1 year ago

Which should be relatively straight forward, but my non-existent experience with fennel (and lisp in general) is making it a bit harder to read the code and figure out what leap actually does and how everything fits together.

; repeat.vim support
; (see the docs in the script:
; https://github.com/tpope/vim-repeat/blob/master/autoload/repeat.vim)
(fn set-dot-repeat* []
  ; Note: dot-repeatable (i.e. non-yank) operation is assumed, we're not
  ; checking it here.
  (let [op vim.v.operator
        cmd (replace-keycodes
              "<cmd>lua require'leap'.leap { dot_repeat = true }<cr>")
        ; We cannot getreg('.') at this point, since the change has not
        ; happened yet - therefore the below hack (thx Sneak).
        change (when (= op :c) (replace-keycodes "<c-r>.<esc>"))
        seq (.. op cmd (or change ""))]
    ; Using pcall, since vim-repeat might not be installed.
    ; Use the same register for the repeated operation.
    (pcall vim.fn.repeat#setreg seq vim.v.register)
    ; Note: we're feeding count inside the seq itself.
    (pcall vim.fn.repeat#set seq -1)))

The compiled Lua code, prettified:

local function set_dot_repeat_2a()
  local op = vim.v.operator
  local cmd = replace_keycodes("<cmd>lua require'leap'.leap { dot_repeat = true }<cr>")
  local change = (op == "c") and replace_keycodes("<c-r>.<esc>") or ""
  local seq = op .. cmd .. change
  pcall(vim.fn["repeat#setreg"], seq, vim.v.register)
  pcall(vim.fn["repeat#set"], seq, -1)
end

That is all, we just call this function in different places in the code, and it will set the dot-repeat action to <operation-trigger> lua require'leap'.leap { dot_repeat = true } <change>? (i.e., seq) with help of vim-repeat. In case of repeating a change operation, we also feed the change itself after the call: <c-r>.<esc> inserts the contents of the . register (:h c_CTRL-R).

The special Leap call with the dot_repeat argument will not prompt for input, but use the state that is saved for operator-pending mode invocations:

; State that is persisted between invocations.
(local state {:args nil  ; arguments passed to the current call
              :source_window nil
              :repeat {:in1 nil
                       :in2 nil}
              ; >>>
              :dot_repeat {:in1 nil
                           :in2 nil
                           :target_idx nil  ; ~ count, so we don't need to feed it in set-dot-repeat*
                           :backward nil
                           :inclusive_op nil
                           :offset nil}
              :saved_editor_opts {}})

The wrapper set-dot-repeat, that is actually used in the main algorithm, updates state before calling set-dot-repeat* itself:

  (fn set-dot-repeat [in1 in2 target_idx]
    (when (and dot-repeatable-op?
               (not (or dot-repeat? (= (type user-given-targets) :table))))
      (set state.dot_repeat {:in1 (and (not user-given-targets) in1)
                             :in2 (and (not user-given-targets) in2)
                             :callback user-given-targets
                             : target_idx
                             : offset
                             : match-xxx*-at-the-end?
                             ; Mind the naming conventions.
                             :backward backward?
                             :inclusive_op inclusive-op?})
      (set-dot-repeat*)))