Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Move some of chat buffer logic to separate file, allow changing windo…
…w layout properly
  • Loading branch information
deathbeam committed Feb 27, 2024
commit 27ba6941538c29fa25f470102ed64484d370c434
53 changes: 53 additions & 0 deletions lua/CopilotChat/chat.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
local Spinner = require('CopilotChat.spinner')
local class = require('CopilotChat.utils').class

local function create_buf()
local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(bufnr, 'copilot-chat')
vim.bo[bufnr].filetype = 'markdown'
vim.treesitter.start(bufnr, 'markdown')
return bufnr
end

local Chat = class(function(self, name)
self.bufnr = create_buf()
self.spinner = Spinner(self.bufnr, name)
end)

function Chat:validate()
if not vim.api.nvim_buf_is_valid(self.bufnr) then
self.bufnr = create_buf()
self.spinner.bufnr = self.bufnr
end
end

function Chat:append(str)
self:validate()

local last_line, last_column = self:last()
vim.api.nvim_buf_set_text(
self.bufnr,
last_line,
last_column,
last_line,
last_column,
vim.split(str, '\n')
)

return self:last()
end

function Chat:last()
self:validate()

local last_line = vim.api.nvim_buf_line_count(self.bufnr) - 1
local last_line_content = vim.api.nvim_buf_get_lines(self.bufnr, -2, -1, false)
local last_column = #last_line_content[1]
return last_line, last_column
end

function Chat:clear()
vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, {})
end

return Chat
10 changes: 7 additions & 3 deletions lua/CopilotChat/copilot.lua
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ local Copilot = class(function(self, show_extra_info)
self.sessionid = nil
self.machineid = machine_id()
self.current_job = nil
self.current_job_on_cancel = nil
end)

--- Ask a question to Copilot
Expand Down Expand Up @@ -164,9 +165,6 @@ function Copilot:ask(prompt, opts)
-- If we already have running job, cancel it and notify the user
if self.current_job then
self:stop()
if on_done then
on_done('job cancelled')
end
end

-- Notify the user about current prompt
Expand All @@ -187,6 +185,8 @@ function Copilot:ask(prompt, opts)
generate_request(self.history, selection, filetype, system_prompt, model, temperature)

local full_response = ''

self.current_job_on_cancel = on_done
self.current_job = curl
.post(url, {
headers = headers,
Expand Down Expand Up @@ -261,6 +261,10 @@ function Copilot:stop()
if self.current_job then
self.current_job:shutdown()
self.current_job = nil
if self.current_job_on_cancel then
self.current_job_on_cancel('job cancelled')
self.current_job_on_cancel = nil
end
end
end

Expand Down
135 changes: 54 additions & 81 deletions lua/CopilotChat/init.lua
Original file line number Diff line number Diff line change
@@ -1,33 +1,18 @@
local log = require('plenary.log')
local Copilot = require('CopilotChat.copilot')
local Spinner = require('CopilotChat.spinner')
local Chat = require('CopilotChat.chat')
local prompts = require('CopilotChat.prompts')
local select = require('CopilotChat.select')
local debuginfo = require('CopilotChat.debuginfo')

local M = {}
local state = {
copilot = nil,
spinner = nil,
chat = nil,
selection = nil,
window = {
id = nil,
bufnr = nil,
},
window = nil,
}

local function is_copilot_reply(input)
return vim.startswith(vim.trim(input), '**' .. M.config.name .. ':** ')
end

local function get_prompt_kind(name)
if vim.startswith(name, 'COPILOT_') then
return 'system'
end

return 'user'
end

local function find_lines_between_separator_at_cursor(bufnr, separator)
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local cursor = vim.api.nvim_win_get_cursor(0)
Expand Down Expand Up @@ -81,47 +66,31 @@ end

local function append(str)
vim.schedule(function()
if not vim.api.nvim_win_is_valid(state.window.id) then
local last_line, last_column = state.chat:append(str)

if not state.window or not vim.api.nvim_win_is_valid(state.window) then
state.copilot:stop()
return
end

local last_line = vim.api.nvim_buf_line_count(state.window.bufnr) - 1
local last_line_content = vim.api.nvim_buf_get_lines(state.window.bufnr, -2, -1, false)
local last_column = #last_line_content[1]
vim.api.nvim_buf_set_text(
state.window.bufnr,
last_line,
last_column,
last_line,
last_column,
vim.split(str, '\n')
)

-- Get new position of text and update cursor
last_line = vim.api.nvim_buf_line_count(state.window.bufnr) - 1
last_line_content = vim.api.nvim_buf_get_lines(state.window.bufnr, -2, -1, false)
last_column = #last_line_content[1]
vim.api.nvim_win_set_cursor(state.window.id, { last_line + 1, last_column })
vim.api.nvim_win_set_cursor(state.window, { last_line + 1, last_column })
end)
end

local function show_help()
if not state.spinner then
if not state.chat then
return
end

state.spinner:finish()

local out = 'Press '

for name, key in pairs(M.config.mappings) do
if key then
out = out .. "'" .. key .. "' to " .. name .. ', \n'
end
end

state.spinner:set(out, -1)
state.chat.spinner:finish()
state.chat.spinner:set(out, -1)
end

local function complete()
Expand Down Expand Up @@ -157,6 +126,10 @@ end
--- Get the prompts to use.
---@param skip_system (boolean?)
function M.get_prompts(skip_system)
local function get_prompt_kind(name)
return vim.startswith(name, 'COPILOT_') and 'system' or 'user'
end

local prompts_to_use = {}

if not skip_system then
Expand Down Expand Up @@ -188,6 +161,8 @@ end
--- Open the chat window.
---@param config (table | nil)
function M.open(config)
local should_reset = config and config.window ~= nil and not vim.tbl_isempty(config.window)

config = vim.tbl_deep_extend('force', M.config, config or {})
local selection = nil
if type(config.selection) == 'function' then
Expand All @@ -199,37 +174,36 @@ function M.open(config)

local just_created = false

if not state.window.bufnr or not vim.api.nvim_buf_is_valid(state.window.bufnr) then
state.window.bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(state.window.bufnr, 'copilot-chat')
vim.bo[state.window.bufnr].filetype = 'markdown'
vim.treesitter.start(state.window.bufnr, 'markdown')
if not state.chat then
state.chat = Chat(M.config.name)
just_created = true

if config.mappings.complete then
vim.keymap.set('i', config.mappings.complete, complete, { buffer = state.window.bufnr })
vim.keymap.set('i', config.mappings.complete, complete, { buffer = state.chat.bufnr })
end

if config.mappings.reset then
vim.keymap.set('n', config.mappings.reset, M.reset, { buffer = state.window.bufnr })
vim.keymap.set('n', config.mappings.reset, M.reset, { buffer = state.chat.bufnr })
end

if config.mappings.close then
vim.keymap.set('n', 'q', M.close, { buffer = state.window.bufnr })
vim.keymap.set('n', 'q', M.close, { buffer = state.chat.bufnr })
end

if config.mappings.submit_prompt then
vim.keymap.set('n', config.mappings.submit_prompt, function()
local input, start_line, end_line, line_count =
find_lines_between_separator_at_cursor(state.window.bufnr, M.config.separator)
if input ~= '' and not is_copilot_reply(input) then
find_lines_between_separator_at_cursor(state.chat.bufnr, M.config.separator)
if
input ~= '' and not not vim.startswith(vim.trim(input), '**' .. M.config.name .. ':** ')
then
-- If we are entering the input at the end, replace it
if line_count == end_line then
vim.api.nvim_buf_set_lines(state.window.bufnr, start_line, end_line, false, { '' })
vim.api.nvim_buf_set_lines(state.chat.bufnr, start_line, end_line, false, { '' })
end
M.ask(input, { selection = state.selection })
end
end, { buffer = state.window.bufnr })
end, { buffer = state.chat.bufnr })
end

if config.mappings.submit_code then
Expand All @@ -244,7 +218,7 @@ function M.open(config)
return
end

local input = find_lines_between_separator_at_cursor(state.window.bufnr, '```')
local input = find_lines_between_separator_at_cursor(state.chat.bufnr, '```')
if input ~= '' then
vim.api.nvim_buf_set_text(
state.selection.buffer,
Expand All @@ -255,15 +229,16 @@ function M.open(config)
vim.split(input, '\n')
)
end
end, { buffer = state.window.bufnr })
end, { buffer = state.chat.bufnr })
end
end

if not state.spinner then
state.spinner = Spinner(state.window.bufnr, M.config.name)
-- Recreate the window if the layout has changed
if should_reset then
M.close()
end

if not state.window.id or not vim.api.nvim_win_is_valid(state.window.id) then
if not state.window or not vim.api.nvim_win_is_valid(state.window) then
local win_opts = {
style = 'minimal',
}
Expand All @@ -284,40 +259,39 @@ function M.open(config)
win_opts.height = math.floor(vim.o.lines * 0.6)
end

state.window.id = vim.api.nvim_open_win(state.window.bufnr, false, win_opts)
vim.wo[state.window.id].wrap = true
vim.wo[state.window.id].linebreak = true
vim.wo[state.window.id].cursorline = true
vim.wo[state.window.id].conceallevel = 2
vim.wo[state.window.id].concealcursor = 'niv'
state.window = vim.api.nvim_open_win(state.chat.bufnr, false, win_opts)
vim.wo[state.window].wrap = true
vim.wo[state.window].linebreak = true
vim.wo[state.window].cursorline = true
vim.wo[state.window].conceallevel = 2
vim.wo[state.window].concealcursor = 'niv'

if just_created then
M.reset()
end
end

vim.api.nvim_set_current_win(state.window.id)
vim.api.nvim_set_current_win(state.window)
end

--- Close the chat window and stop the Copilot model.
function M.close()
if state.window.id and vim.api.nvim_win_is_valid(state.window.id) then
vim.api.nvim_win_close(state.window.id, true)
state.window.id = nil
end
state.copilot:stop()

if state.spinner then
state.spinner:finish()
state.spinner = nil
if state.chat then
state.chat.spinner:finish()
end

state.copilot:stop()
if state.window and vim.api.nvim_win_is_valid(state.window) then
vim.api.nvim_win_close(state.window, true)
state.window = nil
end
end

--- Toggle the chat window.
---@param config (table | nil)
function M.toggle(config)
if state.window.id and vim.api.nvim_win_is_valid(state.window.id) then
if state.window and vim.api.nvim_win_is_valid(state.window) then
M.close()
else
M.open(config)
Expand Down Expand Up @@ -360,7 +334,7 @@ function M.ask(prompt, config)
model = config.model,
temperature = config.temperature,
on_start = function()
state.spinner:start()
state.chat.spinner:start()
append('**' .. M.config.name .. ':** ')
end,
on_done = function()
Expand All @@ -374,12 +348,11 @@ end
--- Reset the chat window and show the help message.
function M.reset()
state.copilot:reset()
if state.window.bufnr and vim.api.nvim_buf_is_valid(state.window.bufnr) then
vim.api.nvim_buf_set_lines(state.window.bufnr, 0, -1, true, {})
if state.chat then
state.chat:clear()
append('\n')
show_help()
end

append('\n')
show_help()
end

M.config = {
Expand Down Expand Up @@ -439,7 +412,7 @@ function M.setup(config)
local logfile = string.format('%s/%s.log', vim.fn.stdpath('state'), 'CopilotChat.nvim')
log.new({
plugin = M.config.name,
level = debug and 'trace' or 'error',
level = M.config.debug and 'trace' or 'warn',
outfile = logfile,
}, true)
log.logfile = logfile
Expand Down