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
28 changes: 1 addition & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ EOF
- **Sticky Prompts** (`> <text>`) - Persist context across single chat session
- **Models** (`$<model>`) - Specify which AI model to use for the chat
- **Prompts** (`/PromptName`) - Use predefined prompt templates for common tasks
- **Selection** - Automatically includes current user selection in prompts

## Examples

Expand Down Expand Up @@ -266,14 +265,14 @@ Types of copilot highlights:

- `CopilotChatHeader` - Header highlight in chat buffer
- `CopilotChatSeparator` - Separator highlight in chat buffer
- `CopilotChatSelection` - Selection highlight in source buffer
- `CopilotChatStatus` - Status and spinner in chat buffer
- `CopilotChatHelp` - Help text in chat buffer
- `CopilotChatResource` - Resource highlight in chat buffer (e.g. `#file`, `#gitdiff`)
- `CopilotChatTool` - Tool call highlight in chat buffer (e.g. `@copilot`)
- `CopilotChatPrompt` - Prompt highlight in chat buffer (e.g. `/Explain`, `/Review`)
- `CopilotChatModel` - Model highlight in chat buffer (e.g. `$gpt-4.1`)
- `CopilotChatUri` - URI highlight in chat buffer (e.g. `##https://...`)
- `CopilotChatSelection` - Selection highlight in source buffer
- `CopilotChatAnnotation` - Annotation highlight in chat buffer (file headers, tool call headers, tool call body)

## Prompts
Expand Down Expand Up @@ -334,27 +333,6 @@ Define your own functions in the configuration with input handling and schema:
}
```

## Selections

Control what content is automatically included:

```lua
{
-- Use visual selection, fallback to current line
selection = function(source)
return require('CopilotChat.select').visual(source) or
require('CopilotChat.select').line(source)
end,
}
```

**Available selections:**

- `require('CopilotChat.select').visual` - Current visual selection
- `require('CopilotChat.select').buffer` - Entire buffer content
- `require('CopilotChat.select').line` - Current line content
- `require('CopilotChat.select').unnamed` - Unnamed register (last deleted/changed/yanked)

## Providers

Add custom AI providers:
Expand Down Expand Up @@ -430,10 +408,6 @@ chat.stop() -- Stop current output
chat.get_source() -- Get the current source buffer and window
chat.set_source(winnr) -- Set the source window

-- Selection Management
chat.get_selection() -- Get the current selection
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
Expand Down
48 changes: 16 additions & 32 deletions lua/CopilotChat/client.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
---@class CopilotChat.client.AskOptions
---@field headless boolean
---@field history table<CopilotChat.client.Message>
---@field selection CopilotChat.select.Selection?
---@field tools table<CopilotChat.client.Tool>?
---@field resources table<CopilotChat.client.Resource>?
---@field system_prompt string
Expand Down Expand Up @@ -32,11 +31,16 @@
---@field description string description of the tool
---@field schema table? schema of the tool

---@class CopilotChat.client.ResourceAnnotations
---@field start_line number?
---@field end_line number?

---@class CopilotChat.client.Resource
---@field data string
---@field name string?
---@field mimetype string?
---@field uri string?
---@field annotations CopilotChat.client.ResourceAnnotations?

---@class CopilotChat.client.Model
---@field provider string?
Expand Down Expand Up @@ -106,29 +110,6 @@ local function generate_resource_block(content, mimetype, name, path, start_line
end
end

--- Generate messages for the given selection
--- @param selection CopilotChat.select.Selection
--- @return CopilotChat.client.Message?
local function generate_selection_message(selection)
local content = selection.content

if not content or content == '' then
return nil
end

return {
content = generate_resource_block(
content,
selection.filetype,
"User's active selection",
selection.filename,
selection.start_line,
selection.end_line
),
role = constants.ROLE.USER,
}
end

--- Generate messages for the given resources
--- @param resources CopilotChat.client.Resource[]
--- @return table<CopilotChat.client.Message>
Expand All @@ -139,8 +120,17 @@ local function generate_resource_messages(resources)
return resource.data and resource.data ~= ''
end)
:map(function(resource)
local start_line = resource.annotations and resource.annotations.start_line or 1
local end_line = resource.annotations and resource.annotations.end_line or nil
return {
content = generate_resource_block(resource.data, resource.mimetype, resource.uri, resource.name, 1, nil),
content = generate_resource_block(
resource.data,
resource.mimetype,
resource.uri,
resource.name,
start_line,
end_line
),
role = constants.ROLE.USER,
}
end)
Expand Down Expand Up @@ -359,20 +349,14 @@ function Client:ask(prompt, opts)
local history = not opts.headless and vim.deepcopy(opts.history) or {}
local tool_calls = utils.ordered_map()
local generated_messages = {}
local selection_message = opts.selection and generate_selection_message(opts.selection)
local resource_messages = generate_resource_messages(opts.resources)

if selection_message then
table.insert(generated_messages, selection_message)
end

if max_tokens then
-- Count required tokens that we cannot reduce
local selection_tokens = selection_message and tiktoken:count(selection_message.content) or 0
local prompt_tokens = tiktoken:count(prompt)
local system_tokens = tiktoken:count(opts.system_prompt)
local resource_tokens = #resource_messages > 0 and tiktoken:count(resource_messages[1].content) or 0
local required_tokens = prompt_tokens + system_tokens + selection_tokens + resource_tokens
local required_tokens = prompt_tokens + system_tokens + resource_tokens

-- Calculate how many tokens we can use for history
local history_limit = max_tokens - required_tokens
Expand Down
6 changes: 2 additions & 4 deletions lua/CopilotChat/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
---@field system_prompt nil|string|fun(source: CopilotChat.source):string
---@field model string?
---@field tools string|table<string>|nil
---@field resources string|table<string>|nil
---@field sticky string|table<string>|nil
---@field language string?
---@field temperature number?
---@field headless boolean?
---@field callback nil|fun(response: CopilotChat.client.Message, source: CopilotChat.source)
---@field remember_as_sticky boolean?
---@field selection false|nil|fun(source: CopilotChat.source):CopilotChat.select.Selection?
---@field window CopilotChat.config.Window?
---@field show_help boolean?
---@field show_folds boolean?
Expand Down Expand Up @@ -58,6 +58,7 @@ return {

model = 'gpt-4.1', -- Default model to use, see ':CopilotChatModels' for available models (can be specified manually in prompt via $).
tools = nil, -- Default tool or array of tools (or groups) to share with LLM (can be specified manually in prompt via @).
resources = 'selection', -- Default resources to share with LLM (can be specified manually in prompt via #).
sticky = nil, -- Default sticky prompt or array of sticky prompts to use at start of every new chat (can be specified manually in prompt via >).
language = 'English', -- Default language to use for answers

Expand All @@ -66,9 +67,6 @@ return {
callback = nil, -- Function called when full response is received
remember_as_sticky = true, -- Remember config as sticky prompts when asking questions

-- default selection
selection = require('CopilotChat.select').visual,

-- default window options
window = {
layout = 'vertical', -- 'vertical', 'horizontal', 'float', 'replace', or a function that returns the layout
Expand Down
29 changes: 28 additions & 1 deletion lua/CopilotChat/config/functions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ local utils = require('CopilotChat.utils')
---@field schema table?
---@field group string?
---@field uri string?
---@field resolve fun(input: table, source: CopilotChat.source, prompt: string):table<CopilotChat.client.Resource>
---@field resolve fun(input: table, source: CopilotChat.source):CopilotChat.client.Resource[]

---@type table<string, CopilotChat.config.functions.Function>
return {
Expand Down Expand Up @@ -214,6 +214,33 @@ return {
end,
},

selection = {
group = 'copilot',
uri = 'neovim://selection',
description = 'Includes the content of the current visual selection. Useful for discussing specific code snippets or text blocks.',

resolve = function(_, source)
utils.schedule_main()
local selection = require('CopilotChat.select').get(source.bufnr)
if not selection then
return {}
end

return {
{
uri = 'neovim://selection',
name = selection.filename,
mimetype = utils.mimetype_to_filetype(selection.filetype),
data = selection.content,
annotations = {
start_line = selection.start_line,
end_line = selection.end_line,
},
},
}
end,
},

quickfix = {
group = 'copilot',
uri = 'neovim://quickfix',
Expand Down
77 changes: 42 additions & 35 deletions lua/CopilotChat/config/mappings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ local async = require('plenary.async')
local copilot = require('CopilotChat')
local client = require('CopilotChat.client')
local constants = require('CopilotChat.constants')
local select = require('CopilotChat.select')
local utils = require('CopilotChat.utils')

---@class CopilotChat.config.mappings.Diff
Expand All @@ -14,23 +15,33 @@ local utils = require('CopilotChat.utils')
---@field bufnr number?

--- Get diff data from a block
---@param bufnr number
---@param block CopilotChat.ui.chat.Block?
---@return CopilotChat.config.mappings.Diff?
local function get_diff(block)
local function get_diff(bufnr, block)
-- If no block found, return nil
if not block then
return nil
end

-- Initialize variables with selection if available
local header = block.header
local selection = copilot.get_selection()
local reference = selection and selection.content
local start_line = selection and selection.start_line
local end_line = selection and selection.end_line
local filename = selection and selection.filename
local filetype = selection and selection.filetype
local bufnr = selection and selection.bufnr
local selection = select.get(bufnr)
local filename = nil
local filetype = nil
local start_line = nil
local end_line = nil
local reference = nil
local bufnr = nil

if selection then
-- If we have a selection, use it as default source of truth
filename = selection.filename
filetype = selection.filetype
start_line = selection.start_line
end_line = selection.end_line
reference = selection.content
bufnr = selection.bufnr
end

-- If we have header info, use it as source of truth
if header.start_line and header.end_line then
Expand Down Expand Up @@ -236,28 +247,30 @@ return {
normal = '<C-y>',
insert = '<C-y>',
callback = function(source)
local diff = get_diff(copilot.chat:get_block(constants.ROLE.ASSISTANT, true))
local diff = get_diff(source.bufnr, copilot.chat:get_block(constants.ROLE.ASSISTANT, true))
diff = prepare_diff_buffer(diff, source)
if not diff then
return
end

local lines = utils.split_lines(diff.change)
vim.api.nvim_buf_set_lines(diff.bufnr, diff.start_line - 1, diff.end_line, false, lines)
copilot.set_selection(diff.bufnr, diff.start_line, diff.end_line)
select.set(source.bufnr, source.winnr, diff.start_line, diff.start_line + #lines - 1)
select.highlight(source.bufnr)
end,
},

jump_to_diff = {
normal = 'gj',
callback = function(source)
local diff = get_diff(copilot.chat:get_block(constants.ROLE.ASSISTANT, true))
local diff = get_diff(source.bufnr, copilot.chat:get_block(constants.ROLE.ASSISTANT, true))
diff = prepare_diff_buffer(diff, source)
if not diff then
return
end

copilot.set_selection(diff.bufnr, diff.start_line, diff.end_line)
select.set(source.bufnr, source.winnr, diff.start_line, diff.end_line)
select.highlight(source.bufnr)
end,
},

Expand Down Expand Up @@ -289,32 +302,26 @@ return {

quickfix_diffs = {
normal = 'gqd',
callback = function()
local selection = copilot.get_selection()
callback = function(source)
local items = {}

for _, message in ipairs(copilot.chat.messages) do
if message.section then
for _, block in ipairs(message.section.blocks) do
local header = block.header

if not header.start_line and selection then
header.filename = selection.filename .. ' (selection)'
header.start_line = selection.start_line
header.end_line = selection.end_line
end
local diff = get_diff(source.bufnr, block)
if diff then
local text = string.format('%s (%s)', diff.filename, diff.filetype)
if diff.start_line and diff.end_line then
text = text .. string.format(' [lines %d-%d]', diff.start_line, diff.end_line)
end

local text = string.format('%s (%s)', header.filename, header.filetype)
if header.start_line and header.end_line then
text = text .. string.format(' [lines %d-%d]', header.start_line, header.end_line)
table.insert(items, {
bufnr = copilot.chat.bufnr,
lnum = block.start_line,
end_lnum = block.end_line,
text = text,
})
end

table.insert(items, {
bufnr = copilot.chat.bufnr,
lnum = block.start_line,
end_lnum = block.end_line,
text = text,
})
end
end

Expand All @@ -341,7 +348,7 @@ return {
normal = 'gd',
full_diff = false, -- Show full diff instead of unified diff when showing diff window
callback = function(source)
local diff = get_diff(copilot.chat:get_block(constants.ROLE.ASSISTANT, true))
local diff = get_diff(source.bufnr, copilot.chat:get_block(constants.ROLE.ASSISTANT, true))
diff = prepare_diff_buffer(diff, source)
if not diff then
return
Expand All @@ -362,7 +369,7 @@ return {
local same_file_diffs = {}
if section then
for _, block in ipairs(section.blocks) do
local block_diff = get_diff(block)
local block_diff = get_diff(source.bufnr, block)
if block_diff and block_diff.bufnr == diff.bufnr then
table.insert(same_file_diffs, block_diff)
end
Expand Down Expand Up @@ -497,7 +504,7 @@ return {
table.insert(lines, '')
end

local selection = copilot.get_selection()
local selection = select.get(source.bufnr)
if selection then
table.insert(lines, '**Selection**')
table.insert(lines, '')
Expand Down
Loading
Loading