zbirenbaum / copilot.lua

Fully featured & enhanced replacement for copilot.vim complete with API for interacting with Github Copilot
MIT License
2.45k stars 67 forks source link

Add a feature to accept til a character #151

Closed rhusiev closed 1 year ago

rhusiev commented 1 year ago

I already love this plugins, as it allows accepting suggestions per word and per line. It would also be awesome if it was possible to use other vim movements. For example, when setting a binding accept_til="<C-g>", the following would be possible(suggestion by copilot I write in the [] brackets):

...
for name, [j in enumerate(names):]
...

Now press <C-g>t(

...
for name, j in enumerate([names):]
...

Something similar can also be added for <C-g>f. Another possibility would be ; to accept til the next occurrence.

It would be very useful in longer line suggestions, where per word acceptance is too small, but accepting the whole line is too much.

I understand that this may be difficult or impossible from the technical view, but I think it is worth looking into.

MunifTanjim commented 1 year ago

The complexity/effort to implement this is too high, and the potential gain is too low... imo.

You can't just do vim motions on virtual texts, so that would need a whole bunch of hacks to implement. And supporting a few motions (e.g. t/f/;) using custom implementation is a rabbit-hole, there will always be another one to implement... can't keep implementing those.

I won't be working on it.


The require("copilot.suggestion").accept function accepts a modifier function to change the suggestion before inserting. This is how accept_line and accept_word are implemented:

https://github.com/zbirenbaum/copilot.lua/blob/decc8d43bcd73a288fa689690c20faf0485da217/lua/copilot/suggestion.lua#L430-L464

You can try to implement it yourself by doing something similar.

MunifTanjim commented 1 year ago

Try something like this:

vim.keymap.set("i", "<C-g>t", function()
  require("copilot.suggestion").accept(function(suggestion)
    local char = vim.fn.getcharstr()

    local range, text = suggestion.range, suggestion.text

    local cursor = vim.api.nvim_win_get_cursor(0)
    local _, character = cursor[1], cursor[2]

    local _, char_idx = string.find(text, char, character + 1, true)
    if char_idx then
      suggestion.text = string.sub(text, 1, char_idx - 1)

      range["end"].line = range["start"].line
      range["end"].character = char_idx - 1
    end
    return suggestion
  end)
end, {
  desc = "[copilot] accept suggestion (t)",
  silent = true,
})
rhusiev commented 1 year ago

I'll look into this, but can't say for sure anything, as neither do I have experience in lua(except making init.lua) nor enough spare time at the moment.

MunifTanjim commented 1 year ago

You can try the snippet from https://github.com/zbirenbaum/copilot.lua/issues/151#issuecomment-1514782725

I think that would work.

rhusiev commented 1 year ago

For some reason when I try to use these functions in by keybindings, it just accepts the whole the whole suggestion. I even tried straight up copying the implementation of word suggestion, it stille accepts everything

MunifTanjim commented 1 year ago

Are you using the latest version? I just tried the snippet from https://github.com/zbirenbaum/copilot.lua/issues/151#issuecomment-1514782725 and it worked as expected for me.

Make sure :Copilot version says copilot.lua decc8d43bcd73a288fa689690c20faf0485da217

rhusiev commented 1 year ago

Oh, ok, thank you!

rhusiev commented 1 year ago

For anybody searching later.

In the end I have this file:

local last_called = { func = nil, char = nil }

local function till()
    if not char then
        char = vim.fn.getcharstr()
    end
    require("copilot.suggestion").accept(function(suggestion)

        local range, text = suggestion.range, suggestion.text

        local cursor = vim.api.nvim_win_get_cursor(0)
        local _, character = cursor[1], cursor[2]

        local _, char_idx = string.find(text, char, character + 1, true)
        if char_idx then
            suggestion.text = string.sub(text, 1, char_idx - 1)

            range["end"].line = range["start"].line
            range["end"].character = char_idx - 1
        else
            -- Leave what is already typed
            suggestion.text = string.sub(text, 1, character)

            range["end"].line = range["start"].line
            range["end"].character = character
        end
        last_called = { func = till, char = char }
        return suggestion
    end)
end

local function find(char)
    if not char then
        char = vim.fn.getcharstr()
    end
    require("copilot.suggestion").accept(function(suggestion)

        local range, text = suggestion.range, suggestion.text

        local cursor = vim.api.nvim_win_get_cursor(0)
        local _, character = cursor[1], cursor[2]

        local _, char_idx = string.find(text, char, character + 1, true)
        if char_idx then
            suggestion.text = string.sub(text, 1, char_idx)

            range["end"].line = range["start"].line
            range["end"].character = char_idx
        else
            -- Leave what is already typed
            suggestion.text = string.sub(text, 1, character)

            range["end"].line = range["start"].line
            range["end"].character = character
        end
        last_called = { func = find, char = char }
        return suggestion
    end)
end

local function call_last()
    if last_called.func then
        last_called.func(last_called.char)
    end
end

vim.keymap.set("i", "<C-m>t", till, {
    desc = "Copilot: accept [t]ill",
    silent = true,
})

vim.keymap.set("i", "<C-m>f", find, {
    desc = "Copilot: accept [f]ind",
    silent = true,
})

vim.keymap.set("i", "<C-m>;", call_last, {
    desc = "Copilot: accept last",
    silent = true,
})

When pressing <C-m>t and some symbol, it will accept till that character, not including <C-m>f finds that symbol and accepts, including it <C-m>; repeats the last of those 2 with the last used character

rhusiev commented 1 year ago

I am just not sure, whether it is a good practice to use

local last_called = { func = nil, char = nil }

as a way to save the last function called, as I really don't know how lua works, especially with neovim

MunifTanjim commented 1 year ago

I am just not sure, whether it is a good practice to use

local last_called = { func = nil, char = nil }

as a way to save the last function called, as I really don't know how lua works, especially with neovim

That's okay. If it's working, it's a perfectly acceptable thing to do.


But you forgot to add char parameter to the till function:

local function till()

should be

local function till(char)

right?

rhusiev commented 1 year ago

Oops, right

rhusiev commented 1 year ago

Thank you a lot for help