nvimでcopilot chatの最後のコードブロックを選択する

CopilotChatをNeovimで使用するにあたり、コードブロックの選択が面倒だったので選択してくれるものを作成しました。

github.com

同じ悲しみを背負っている方に共有したいとおもいます。

-- select codeblock text
local function move_cursor_to_above_codeblock()
  local current_line_number = vim.api.nvim_win_get_cursor(0)[1]
  local start_line = current_line_number
  -- search above
  for i = current_line_number, 0, -1 do
    local _line = vim.fn.getline(i)
    if string.match(_line, "^```") then
      start_line = i - 1
      break
    end
    if i <= 1 then
      start_line = 1
      break
    end
  end
  vim.api.nvim_win_set_cursor(0, {start_line, 0})
end

local function select_codeblock_text(cursor_position)
  local current_line_number = vim.api.nvim_win_get_cursor(0)[1]
  local start_line = current_line_number
  local end_line = current_line_number
  local max_line = vim.api.nvim_buf_line_count(0)
  -- search above
  for i = current_line_number, 0, -1 do
    local _line = vim.fn.getline(i)
    if string.match(_line, "^```") then
      start_line = i + 1
      break
    end
    if i == 1 then
      start_line = 1
      break
    end
  end
  -- search below
  for i = current_line_number, max_line do
    local _line = vim.fn.getline(i)
    if string.match(_line, "^```") then
      end_line = i - 1
      break
    end
  end
  if start_line <= 1 then
    print("no codeblock text")
    -- restore cursor position
    vim.api.nvim_win_set_cursor(0, {cursor_position, 0})
  else
    -- select lines
    vim.api.nvim_win_set_cursor(0, {start_line, 0})
    vim.cmd("normal! V")
    vim.api.nvim_win_set_cursor(0, {end_line, 0})
  end
end

local function select_last_codeblock_text()
  -- save cursor position
  local cursor_position = vim.api.nvim_win_get_cursor(0)[1]
  vim.cmd("normal! G")
  move_cursor_to_above_codeblock()
  select_codeblock_text(cursor_position)
end
vim.keymap.set("n", "<leader>vmm", select_last_codeblock_text, {desc = "select codeblock text (last)", noremap = true})

copilot chatの提案を受けたあと、<leader> + v + m + mで最後のコードブロックの中身が選択されます。

また、以下を追加することで<leader> + v + m + dとすると変更前、変更後のdiffを取ることが出来ます。

注意点としては/tmpにファイルを吐き出すことと、無名レジスタを使用するところです。

※tmpなのでファイルのお掃除もせず、毎回上書きです

local function save_yanked_text(path, reg)
  local text = vim.fn.getreg(reg)
  if text == nil or text == "" then
    print("no text in register")
    return false
  end
  local file = io.open(path, "w")
  if file == nil then
    print("cannot open file")
    return false
  end
  file:write(text)
  file:close()
  return true
end

local function get_filetype_from_codeblock()
  vim.cmd("normal! G")
  move_cursor_to_above_codeblock()
  local current_line_number = vim.api.nvim_win_get_cursor(0)[1]
  local block_line = current_line_number
  for i = current_line_number, 0, -1 do
    local _line = vim.fn.getline(i)
    if string.match(_line, "^```") then
      block_line = i
      break
    end
  end
  vim.api.nvim_win_set_cursor(0, {block_line, 0})

  local filetype = vim.api.nvim_get_current_line()
  -- extract filetype from codeblock
  filetype = filetype:match("^```(%w+)")
  return filetype
end

local function diff_codeblock_text()
  local cursor_position = vim.api.nvim_win_get_cursor(0)[1]
  vim.cmd("normal! G")
  move_cursor_to_above_codeblock()
  local current_line_number = vim.api.nvim_win_get_cursor(0)[1]
  local start_line = current_line_number
  for i = current_line_number, 0, -1 do
    local _line = vim.fn.getline(i)
    if string.match(_line, "^```") then
      start_line = i + 1
      break
    end
    if i == 1 then
      start_line = 1
      break
    end
  end
  if start_line <= 1 then
    print("no codeblock in this buffer")
    -- restore cursor position
    vim.api.nvim_win_set_cursor(0, {cursor_position, 0})
    return
  end
  local target_text = "/tmp/_target_text"
  local copilot_text = "/tmp/_copilot_suggestion"
  local function save_and_check(path, register)
    local result = save_yanked_text(path, register)
    if not result then
      error("Failed to save yanked text to " .. path)
    end
  end
  local function diff_texts(target_text, copilot_text, filetype)
    -- open copilot_text text to new tab
    vim.cmd("tabnew " .. copilot_text)
    vim.cmd("setlocal filetype=" .. filetype)
    vim.cmd("vertical diffsplit " .. target_text)
    vim.cmd("setlocal filetype=" .. filetype)
  end
  save_and_check(target_text, '"')
  local filetype = get_filetype_from_codeblock()
  select_last_codeblock_text()
  vim.cmd('normal! y')
  save_and_check(copilot_text, '"')
  diff_texts(target_text, copilot_text, filetype)
end
vim.keymap.set("n", "<leader>vmd", diff_codeblock_text, {desc = "diff codeblock text", noremap = true})
vim.keymap.set("n", "<leader>vmc", ":tabclose<CR>", {desc = "close diff tab", noremap = true})

ついさっき思いついたものなので作りは荒いですが、参考になれば幸いです。