Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,16 +436,10 @@ chat.set_selection(bufnr, start_line, end_line, clear) -- Set or clear selection
-- Prompt & Model Management
chat.select_prompt(config) -- Open prompt selector with optional config
chat.select_model() -- Open model selector
chat.prompts() -- Get all available prompts

-- Completion
chat.trigger_complete() -- Trigger completion in chat window
chat.complete_info() -- Get completion info for custom providers
chat.complete_items() -- Get completion items (WARN: async, requires plenary.async.run)

-- History Management
chat.save(name, history_path) -- Save chat history
chat.load(name, history_path) -- Load chat history
chat.save(name, history_path) -- Save chat history

-- Configuration
chat.setup(config) -- Update configuration
Expand Down
235 changes: 235 additions & 0 deletions lua/CopilotChat/completion.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
local async = require('plenary.async')
local client = require('CopilotChat.client')
local config = require('CopilotChat.config')
local functions = require('CopilotChat.functions')
local utils = require('CopilotChat.utils')

local M = {}

--- Get the completion info for the chat window, for use with custom completion providers
---@return table
function M.info()
return {
triggers = { '@', '/', '#', '$' },
pattern = [[\%(@\|/\|#\|\$\)\S*]],
}
end

--- Get the completion items for the chat window, for use with custom completion providers
---@return table
---@async
function M.items()
local models = client:models()
local prompts = config.prompts or {}
local items = {}

for name, prompt in pairs(prompts) do
if type(prompt) == 'string' then
prompt = {
prompt = prompt,
}
end

local kind = ''
local info = ''
if prompt.prompt then
kind = 'user'
info = prompt.prompt
elseif prompt.system_prompt then
kind = 'system'
info = prompt.system_prompt
end

items[#items + 1] = {
word = '/' .. name,
abbr = name,
kind = kind,
info = info,
menu = prompt.description or '',
icase = 1,
dup = 0,
empty = 0,
}
end

for id, model in pairs(models) do
items[#items + 1] = {
word = '$' .. id,
abbr = id,
kind = model.provider,
menu = model.name,
icase = 1,
dup = 0,
empty = 0,
}
end

local groups = {}
for name, tool in pairs(config.functions) do
if tool.group then
groups[tool.group] = groups[tool.group] or {}
groups[tool.group][name] = tool
end
end
for name, group in pairs(groups) do
local group_tools = vim.tbl_keys(group)
items[#items + 1] = {
word = '@' .. name,
abbr = name,
kind = 'group',
info = table.concat(group_tools, '\n'),
menu = string.format('%s tools', #group_tools),
icase = 1,
dup = 0,
empty = 0,
}
end
for name, tool in pairs(config.functions) do
items[#items + 1] = {
word = '@' .. name,
abbr = name,
kind = 'tool',
info = tool.description,
menu = tool.group or '',
icase = 1,
dup = 0,
empty = 0,
}
end

local tools_to_use = functions.parse_tools(config.functions)
for _, tool in pairs(tools_to_use) do
local uri = config.functions[tool.name].uri
if uri then
local info =
string.format('%s\n\n%s', tool.description, tool.schema and vim.inspect(tool.schema, { indent = ' ' }) or '')

items[#items + 1] = {
word = '#' .. tool.name,
abbr = tool.name,
kind = config.functions[tool.name].group or 'resource',
info = info,
menu = uri,
icase = 1,
dup = 0,
empty = 0,
}
end
end

table.sort(items, function(a, b)
if a.kind == b.kind then
return a.word < b.word
end
return a.kind < b.kind
end)

return items
end

--- Trigger the completion for the chat window.
---@param without_input boolean?
function M.complete(without_input)
local source = require('CopilotChat').get_source()
local info = M.info()
local bufnr = vim.api.nvim_get_current_buf()
local line = vim.api.nvim_get_current_line()
local win = vim.api.nvim_get_current_win()
local row, col = unpack(vim.api.nvim_win_get_cursor(win))

local prefix, cmp_start = unpack(vim.fn.matchstrpos(line:sub(1, col), info.pattern))
if not prefix then
return
end

if not without_input and vim.startswith(prefix, '#') and vim.endswith(prefix, ':') then
local found_tool = config.functions[prefix:sub(2, -2)]
local found_schema = found_tool and functions.parse_schema(found_tool)
if found_tool and found_schema then
async.run(function()
local value = functions.enter_input(found_schema, source)
if not value then
return
end

utils.schedule_main()
vim.api.nvim_buf_set_text(bufnr, row - 1, col, row - 1, col, { value })
vim.api.nvim_win_set_cursor(0, { row, col + #value })
end)
end

return
end

utils.debounce('copilot_chat_complete', function()
async.run(function()
local items = M.items()
utils.schedule_main()

local row_changed = vim.api.nvim_win_get_cursor(win)[1] ~= row
local mode = vim.api.nvim_get_mode().mode
if row_changed or not (mode == 'i' or mode == 'ic') then
return
end

vim.fn.complete(
cmp_start + 1,
vim.tbl_filter(function(item)
return vim.startswith(item.word:lower(), prefix:lower())
end, items)
)
end)
end, 100)
end

--- Omnifunc for the chat window completion.
---@param findstart integer 0 or 1, decides behavior
---@param base integer findstart=0, text to match against
---@param _ any
---@return number|table
function M.omnifunc(findstart, base)
assert(base)
local bufnr = vim.api.nvim_get_current_buf()
local ft = vim.bo[bufnr].filetype

if ft ~= 'copilot-chat' then
return findstart == 1 and -1 or {}
end

M.complete(true)
return -2 -- Return -2 to indicate that we are handling the completion asynchronously
end

--- Enable the completion for specific buffer.
---@param bufnr number: the buffer number to enable completion for
---@param autocomplete boolean: whether to enable autocomplete
function M.enable(bufnr, autocomplete)
if autocomplete then
vim.api.nvim_create_autocmd('TextChangedI', {
buffer = bufnr,
callback = function()
local completeopt = vim.opt.completeopt:get()
if not vim.tbl_contains(completeopt, 'noinsert') and not vim.tbl_contains(completeopt, 'noselect') then
-- Don't trigger completion if completeopt is not set to noinsert or noselect
return
end

M.complete(true)
end,
})

-- Add noinsert completeopt if not present
if vim.fn.has('nvim-0.11.0') == 1 then
local completeopt = vim.opt.completeopt:get()
if not vim.tbl_contains(completeopt, 'noinsert') then
table.insert(completeopt, 'noinsert')
vim.bo[bufnr].completeopt = table.concat(completeopt, ',')
end
end
else
-- Just set the omnifunc for the buffer
vim.bo[bufnr].omnifunc = [[v:lua.require'CopilotChat.completion'.omnifunc]]
end
end

return M
2 changes: 1 addition & 1 deletion lua/CopilotChat/config/mappings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ return {
complete = {
insert = '<Tab>',
callback = function()
copilot.trigger_complete()
require('CopilotChat.completion').complete()
end,
},

Expand Down
Loading
Loading