From 55722d7b3131ef6d2441e379d9c64c5991b0cf5c Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Sat, 16 Mar 2024 03:47:53 +0100 Subject: [PATCH] feat: Switch from virt text to virt lines for help (#172) Allows nice multiline display Signed-off-by: Tomas Slusny --- lua/CopilotChat/chat.lua | 21 ++++++- lua/CopilotChat/diff.lua | 26 +++++--- lua/CopilotChat/init.lua | 117 +++++++++++++++++------------------- lua/CopilotChat/spinner.lua | 57 +++++++++++------- 4 files changed, 126 insertions(+), 95 deletions(-) diff --git a/lua/CopilotChat/chat.lua b/lua/CopilotChat/chat.lua index 03aeef6b..fc0e0943 100644 --- a/lua/CopilotChat/chat.lua +++ b/lua/CopilotChat/chat.lua @@ -11,6 +11,7 @@ ---@field close fun(self: CopilotChat.Chat) ---@field focus fun(self: CopilotChat.Chat) ---@field follow fun(self: CopilotChat.Chat) +---@field finish fun(self: CopilotChat.Chat) local Spinner = require('CopilotChat.spinner') local utils = require('CopilotChat.utils') @@ -38,7 +39,9 @@ local function create_buf() return bufnr end -local Chat = class(function(self, on_buf_create) +local Chat = class(function(self, ns, help, on_buf_create) + self.ns = ns + self.help = help self.on_buf_create = on_buf_create self.bufnr = nil self.spinner = nil @@ -62,7 +65,7 @@ function Chat:validate() self.bufnr = create_buf() if not self.spinner then - self.spinner = Spinner(self.bufnr, 'copilot-chat') + self.spinner = Spinner(self.bufnr, self.ns, 'copilot-chat') else self.spinner.bufnr = self.bufnr end @@ -87,6 +90,11 @@ end function Chat:append(str) self:validate() + + if self.spinner then + self.spinner:start() + end + local last_line, last_column, _ = self:last() vim.api.nvim_buf_set_text( self.bufnr, @@ -162,6 +170,7 @@ function Chat:open(config) vim.wo[self.winnr].conceallevel = 2 vim.wo[self.winnr].concealcursor = 'niv' vim.wo[self.winnr].foldlevel = 99 + vim.wo[self.winnr].relativenumber = false if config.show_folds then vim.wo[self.winnr].foldcolumn = '1' vim.wo[self.winnr].foldmethod = 'expr' @@ -200,4 +209,12 @@ function Chat:follow() vim.api.nvim_win_set_cursor(self.winnr, { last_line + 1, last_column }) end +function Chat:finish() + if not self.spinner then + return + end + + self.spinner:finish(self.help, true) +end + return Chat diff --git a/lua/CopilotChat/diff.lua b/lua/CopilotChat/diff.lua index 3f54dd07..98d26d55 100644 --- a/lua/CopilotChat/diff.lua +++ b/lua/CopilotChat/diff.lua @@ -34,16 +34,24 @@ local function create_buf() return bufnr end -local Diff = class(function(self, on_buf_create, help) +local Diff = class(function(self, ns, help, on_buf_create) + self.ns = ns self.help = help self.on_buf_create = on_buf_create self.current = nil - self.ns = vim.api.nvim_create_namespace('copilot-diff') - self.mark_ns = vim.api.nvim_create_namespace('copilot-diff-mark') + self.hl_ns = vim.api.nvim_create_namespace('copilot-diff-mark') self.bufnr = nil - vim.api.nvim_set_hl(self.ns, '@diff.plus', { bg = blend_color_with_neovim_bg('DiffAdd', 20) }) - vim.api.nvim_set_hl(self.ns, '@diff.minus', { bg = blend_color_with_neovim_bg('DiffDelete', 20) }) - vim.api.nvim_set_hl(self.ns, '@diff.delta', { bg = blend_color_with_neovim_bg('DiffChange', 20) }) + vim.api.nvim_set_hl(self.hl_ns, '@diff.plus', { bg = blend_color_with_neovim_bg('DiffAdd', 20) }) + vim.api.nvim_set_hl( + self.hl_ns, + '@diff.minus', + { bg = blend_color_with_neovim_bg('DiffDelete', 20) } + ) + vim.api.nvim_set_hl( + self.hl_ns, + '@diff.delta', + { bg = blend_color_with_neovim_bg('DiffChange', 20) } + ) end) function Diff:valid() @@ -75,13 +83,13 @@ function Diff:show(a, b, filetype, winnr) diff = '\n' .. diff vim.api.nvim_win_set_buf(winnr, self.bufnr) - vim.api.nvim_win_set_hl_ns(winnr, self.ns) + vim.api.nvim_win_set_hl_ns(winnr, self.hl_ns) vim.bo[self.bufnr].modifiable = true vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, vim.split(diff, '\n')) vim.bo[self.bufnr].modifiable = false local opts = { - id = self.mark_ns, + id = self.ns, hl_mode = 'combine', priority = 100, virt_text = { { self.help, 'CursorColumn' } }, @@ -92,7 +100,7 @@ function Diff:show(a, b, filetype, winnr) opts.virt_text_pos = 'inline' end - vim.api.nvim_buf_set_extmark(self.bufnr, self.mark_ns, 0, 0, opts) + vim.api.nvim_buf_set_extmark(self.bufnr, self.ns, 0, 0, opts) local ok, parser = pcall(vim.treesitter.get_parser, self.bufnr, 'diff') if ok and parser then vim.treesitter.start(self.bufnr, 'diff') diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index f993c023..6ef9fbf9 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -111,24 +111,6 @@ local function append(str) end) end -local function show_help() - local out = 'Press ' - for name, key in pairs(M.config.mappings) do - if key then - out = out .. "'" .. key .. "' to " .. name:gsub('_', ' ') .. ', ' - end - end - - out = out - .. 'use @' - .. M.config.mappings.complete - .. ' or /' - .. M.config.mappings.complete - .. ' for different options.' - state.chat.spinner:finish() - state.chat.spinner:set(out, -1) -end - local function complete() local line = vim.api.nvim_get_current_line() local col = vim.api.nvim_win_get_cursor(0)[2] @@ -325,7 +307,6 @@ function M.ask(prompt, config, source) append(updated_prompt) append('\n\n **' .. config.name .. '** ' .. config.separator .. '\n\n') state.chat:follow() - state.chat.spinner:start() local selected_context = config.context if string.find(prompt, '@buffers') then @@ -339,7 +320,7 @@ function M.ask(prompt, config, source) append('\n\n **Error** ' .. config.separator .. '\n\n') append('```\n' .. err .. '\n```') append('\n\n' .. config.separator .. '\n\n') - show_help() + state.chat:finish() end context.find_for_query(state.copilot, { @@ -365,7 +346,7 @@ function M.ask(prompt, config, source) append('\n\n' .. token_count .. ' tokens used') end append('\n\n' .. config.separator .. '\n\n') - show_help() + state.chat:finish() end, on_progress = function(token) append(token) @@ -380,7 +361,7 @@ function M.reset() state.copilot:reset() state.chat:clear() append('\n') - show_help() + state.chat:finish() end --- Enables/disables debug @@ -401,48 +382,62 @@ end function M.setup(config) M.config = vim.tbl_deep_extend('force', default_config, config or {}) state.copilot = Copilot(M.config.proxy, M.config.allow_insecure) + local mark_ns = vim.api.nvim_create_namespace('copilot-chat') - state.diff = Diff( - function(bufnr) - if M.config.mappings.close then - vim.keymap.set('n', M.config.mappings.close, function() - state.diff:restore(state.chat.winnr, state.chat.bufnr) - end, { buffer = bufnr }) - end - if M.config.mappings.accept_diff then - vim.keymap.set('n', M.config.mappings.accept_diff, function() - local selection = get_selection() - if not selection.start_row or not selection.end_row then - return - end + local diff_help = "'" + .. M.config.mappings.close + .. "' to close diff.\n'" + .. M.config.mappings.accept_diff + .. "' to accept diff." - local current = state.diff.current - if not current then - return - end + state.diff = Diff(mark_ns, diff_help, function(bufnr) + if M.config.mappings.close then + vim.keymap.set('n', M.config.mappings.close, function() + state.diff:restore(state.chat.winnr, state.chat.bufnr) + end, { buffer = bufnr }) + end + if M.config.mappings.accept_diff then + vim.keymap.set('n', M.config.mappings.accept_diff, function() + local selection = get_selection() + if not selection.start_row or not selection.end_row then + return + end - local lines = vim.split(current, '\n') - if #lines > 0 then - vim.api.nvim_buf_set_text( - state.source.bufnr, - selection.start_row - 1, - selection.start_col - 1, - selection.end_row - 1, - selection.end_col, - lines - ) - end - end, { buffer = bufnr }) - end - end, - "Press '" - .. M.config.mappings.close - .. "' to close diff, '" - .. M.config.mappings.accept_diff - .. "' to accept diff." - ) - - state.chat = Chat(function(bufnr) + local current = state.diff.current + if not current then + return + end + + local lines = vim.split(current, '\n') + if #lines > 0 then + vim.api.nvim_buf_set_text( + state.source.bufnr, + selection.start_row - 1, + selection.start_col - 1, + selection.end_row - 1, + selection.end_col, + lines + ) + end + end, { buffer = bufnr }) + end + end) + + local chat_help = '' + for name, key in pairs(M.config.mappings) do + if key then + chat_help = chat_help .. "'" .. key .. "' to " .. name:gsub('_', ' ') .. '\n' + end + end + + chat_help = chat_help + .. '@' + .. M.config.mappings.complete + .. ' or /' + .. M.config.mappings.complete + .. ' for different completion options.' + + state.chat = Chat(mark_ns, chat_help, function(bufnr) if M.config.mappings.complete then vim.keymap.set('i', M.config.mappings.complete, complete, { buffer = bufnr }) end diff --git a/lua/CopilotChat/spinner.lua b/lua/CopilotChat/spinner.lua index 48ff0723..ecd9eba5 100644 --- a/lua/CopilotChat/spinner.lua +++ b/lua/CopilotChat/spinner.lua @@ -1,6 +1,6 @@ ---@class CopilotChat.Spinner ---@field bufnr number ----@field set fun(self: CopilotChat.Spinner, text: string, offset: number) +---@field set fun(self: CopilotChat.Spinner, text: string, virt_line: boolean) ---@field start fun(self: CopilotChat.Spinner) ---@field finish fun(self: CopilotChat.Spinner) @@ -21,45 +21,50 @@ local spinner_frames = { '⠏', } -local Spinner = class(function(self, bufnr, title) - self.ns = vim.api.nvim_create_namespace('copilot-spinner') +local Spinner = class(function(self, bufnr, ns, title) + self.ns = ns self.bufnr = bufnr self.title = title self.timer = nil self.index = 1 end) -function Spinner:set(text, offset) - offset = offset or 0 - +function Spinner:set(text, virt_line) vim.schedule(function() if not vim.api.nvim_buf_is_valid(self.bufnr) then self:finish() return end - local line = vim.api.nvim_buf_line_count(self.bufnr) - 1 + offset - line = math.max(0, line) + local line = vim.api.nvim_buf_line_count(self.bufnr) - 1 local opts = { id = self.ns, hl_mode = 'combine', priority = 100, - virt_text = vim.tbl_map(function(t) - return { t, 'CursorColumn' } - end, vim.split(text, '\n')), } - -- stable do not supports virt_text_pos - if not is_stable() then - opts.virt_text_pos = offset ~= 0 and 'inline' or 'eol' + if virt_line then + line = line - 1 + opts.virt_lines_leftcol = true + opts.virt_lines = vim.tbl_map(function(t) + return { { '| ' .. t, 'DiagnosticInfo' } } + end, vim.split(text, '\n')) + else + opts.virt_text = vim.tbl_map(function(t) + return { t, 'CursorColumn' } + end, vim.split(text, '\n')) end - vim.api.nvim_buf_set_extmark(self.bufnr, self.ns, line, 0, opts) + vim.api.nvim_buf_set_extmark(self.bufnr, self.ns, math.max(0, line), 0, opts) end) end function Spinner:start() + if self.timer then + return + end + self.timer = vim.loop.new_timer() self.timer:start(0, 100, function() self:set(spinner_frames[self.index]) @@ -67,21 +72,27 @@ function Spinner:start() end) end -function Spinner:finish() - if self.timer then +function Spinner:finish(msg, offset) + vim.schedule(function() + if not self.timer then + return + end + self.timer:stop() self.timer:close() self.timer = nil - vim.schedule(function() - if not vim.api.nvim_buf_is_valid(self.bufnr) then - return - end + if not vim.api.nvim_buf_is_valid(self.bufnr) then + return + end + if msg then + self:set(msg, offset) + else vim.api.nvim_buf_del_extmark(self.bufnr, self.ns, self.ns) vim.notify('Done!', vim.log.levels.INFO, { title = self.title }) - end) - end + end + end) end return Spinner