From 0afce8b714ba8789b7a382656934a2a99b51a863 Mon Sep 17 00:00:00 2001 From: Tomas Slusny Date: Mon, 18 Aug 2025 23:16:36 +0200 Subject: [PATCH] refactor(core): remove selection API in favor of resources Replaces the selection API with a unified resources mechanism for sharing context with the LLM. Updates config, mappings, and internal logic to use resources instead of selection. Deprecates selection-related functions and highlights. Improves flexibility and future extensibility for context injection. --- README.md | 28 +---- lua/CopilotChat/client.lua | 48 +++------ lua/CopilotChat/config.lua | 6 +- lua/CopilotChat/config/functions.lua | 29 +++++- lua/CopilotChat/config/mappings.lua | 77 +++++++------- lua/CopilotChat/init.lua | 125 ++++++---------------- lua/CopilotChat/select.lua | 149 ++++++++++++++------------- plugin/CopilotChat.lua | 26 ++--- 8 files changed, 214 insertions(+), 274 deletions(-) diff --git a/README.md b/README.md index 7dbba797..2be9d184 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,6 @@ EOF - **Sticky Prompts** (`> `) - Persist context across single chat session - **Models** (`$`) - 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 @@ -266,6 +265,7 @@ 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`) @@ -273,7 +273,6 @@ Types of copilot highlights: - `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 @@ -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: @@ -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 diff --git a/lua/CopilotChat/client.lua b/lua/CopilotChat/client.lua index f2dba5b0..6121bdfe 100644 --- a/lua/CopilotChat/client.lua +++ b/lua/CopilotChat/client.lua @@ -1,7 +1,6 @@ ---@class CopilotChat.client.AskOptions ---@field headless boolean ---@field history table ----@field selection CopilotChat.select.Selection? ---@field tools table? ---@field resources table? ---@field system_prompt string @@ -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? @@ -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 @@ -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) @@ -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 diff --git a/lua/CopilotChat/config.lua b/lua/CopilotChat/config.lua index 1a739c67..d3f2b8cb 100644 --- a/lua/CopilotChat/config.lua +++ b/lua/CopilotChat/config.lua @@ -17,13 +17,13 @@ ---@field system_prompt nil|string|fun(source: CopilotChat.source):string ---@field model string? ---@field tools string|table|nil +---@field resources string|table|nil ---@field sticky string|table|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? @@ -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 @@ -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 diff --git a/lua/CopilotChat/config/functions.lua b/lua/CopilotChat/config/functions.lua index 9050d267..eb067376 100644 --- a/lua/CopilotChat/config/functions.lua +++ b/lua/CopilotChat/config/functions.lua @@ -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 +---@field resolve fun(input: table, source: CopilotChat.source):CopilotChat.client.Resource[] ---@type table return { @@ -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', diff --git a/lua/CopilotChat/config/mappings.lua b/lua/CopilotChat/config/mappings.lua index e3e2e248..ec42be85 100644 --- a/lua/CopilotChat/config/mappings.lua +++ b/lua/CopilotChat/config/mappings.lua @@ -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 @@ -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 @@ -236,7 +247,7 @@ return { normal = '', insert = '', 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 @@ -244,20 +255,22 @@ return { 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, }, @@ -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 @@ -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 @@ -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 @@ -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, '') diff --git a/lua/CopilotChat/init.lua b/lua/CopilotChat/init.lua index 38ee5568..ba32542e 100644 --- a/lua/CopilotChat/init.lua +++ b/lua/CopilotChat/init.lua @@ -4,6 +4,7 @@ local functions = require('CopilotChat.functions') local client = require('CopilotChat.client') local constants = require('CopilotChat.constants') local notify = require('CopilotChat.notify') +local select = require('CopilotChat.select') local utils = require('CopilotChat.utils') local WORD = '([^%s:]+)' @@ -25,18 +26,22 @@ local M = setmetatable({}, { }) --- @class CopilotChat.source ---- @field bufnr number ---- @field winnr number +--- @field bufnr number? +--- @field winnr number? --- @field cwd fun():string --- @class CopilotChat.state --- @field source CopilotChat.source? --- @field sticky string[]? local state = { - -- Current state tracking - source = nil, + source = { + bufnr = nil, + winnr = nil, + cwd = function() + return '.' + end, + }, - -- Last state tracking sticky = nil, } @@ -80,6 +85,12 @@ local function insert_sticky(prompt, config) end end + if config.remember_as_sticky and config.resources and not vim.deep_equal(config.resources, M.config.resources) then + for _, resource in ipairs(utils.to_table(config.resources)) do + stickies:set('#' .. resource, true) + end + end + if config.remember_as_sticky and config.system_prompt @@ -129,27 +140,6 @@ local function store_sticky(prompt) state.sticky = sticky end ---- Update the highlights for chat buffer -local function update_highlights() - local selection_ns = vim.api.nvim_create_namespace('copilot-chat-selection') - for _, buf in ipairs(vim.api.nvim_list_bufs()) do - vim.api.nvim_buf_clear_namespace(buf, selection_ns, 0, -1) - end - - if M.chat.config.highlight_selection and M.chat:focused() then - local selection = M.get_selection() - if not selection or not utils.buf_valid(selection.bufnr) or not selection.start_line or not selection.end_line then - return - end - - vim.api.nvim_buf_set_extmark(selection.bufnr, selection_ns, selection.start_line - 1, 0, { - hl_group = 'CopilotChatSelection', - end_row = selection.end_line, - strict = false, - }) - end -end - --- List available models. --- @return CopilotChat.client.Model[] local function list_models() @@ -315,6 +305,16 @@ function M.resolve_functions(prompt, config) tools[tool.name] = tool end + if config.resources then + local resources = utils.to_table(config.resources) + local lines = utils.split_lines(prompt) + for i = #resources, 1, -1 do + local resource = resources[i] + table.insert(lines, 1, '#' .. resource) + end + prompt = table.concat(lines, '\n') + end + local enabled_tools = {} local resolved_resources = {} local resolved_tools = {} @@ -413,7 +413,7 @@ function M.resolve_functions(prompt, config) local schema = tools[name] and tools[name].schema or nil local result = '' - local ok, output = pcall(tool.resolve, functions.parse_input(input, schema), state.source or {}, prompt) + local ok, output = pcall(tool.resolve, functions.parse_input(input, schema), state.source) if not ok then result = string.format(BLOCK_OUTPUT_FORMAT, 'error', utils.make_string(output)) else @@ -527,9 +527,7 @@ function M.resolve_prompt(prompt, config) config.system_prompt = config.system_prompt .. '\n' .. M.config.prompts.COPILOT_BASE.system_prompt config.system_prompt = config.system_prompt:gsub('{OS_NAME}', jit.os) config.system_prompt = config.system_prompt:gsub('{LANGUAGE}', config.language) - if state.source then - config.system_prompt = config.system_prompt:gsub('{DIR}', state.source.cwd()) - end + config.system_prompt = config.system_prompt:gsub('{DIR}', state.source.cwd()) end return config, prompt @@ -592,55 +590,6 @@ function M.set_source(source_winnr) return false end ---- Get the selection from the source buffer. ----@return CopilotChat.select.Selection? -function M.get_selection() - local config = vim.tbl_deep_extend('force', M.config, M.chat.config) - local selection = config.selection - local bufnr = state.source and state.source.bufnr - local winnr = state.source and state.source.winnr - - if selection and utils.buf_valid(bufnr) and winnr and vim.api.nvim_win_is_valid(winnr) then - return selection(state.source) - end - - return nil -end - ---- Sets the selection to specific lines in buffer. ----@param bufnr number ----@param start_line number ----@param end_line number ----@param clear boolean? -function M.set_selection(bufnr, start_line, end_line, clear) - if not utils.buf_valid(bufnr) then - return - end - - if clear then - for _, mark in ipairs({ '<', '>', '[', ']' }) do - pcall(vim.api.nvim_buf_del_mark, bufnr, mark) - end - update_highlights() - return - end - - local winnr = vim.fn.win_findbuf(bufnr)[1] - if not winnr and state.source then - winnr = state.source.winnr - end - if not winnr then - return - end - - pcall(vim.api.nvim_buf_set_mark, bufnr, '<', start_line, 0, {}) - pcall(vim.api.nvim_buf_set_mark, bufnr, '>', end_line, 0, {}) - pcall(vim.api.nvim_buf_set_mark, bufnr, '[', start_line, 0, {}) - pcall(vim.api.nvim_buf_set_mark, bufnr, ']', end_line, 0, {}) - pcall(vim.api.nvim_win_set_cursor, winnr, { start_line, 0 }) - update_highlights() -end - --- Open the chat window. ---@param config CopilotChat.config.Shared? function M.open(config) @@ -667,7 +616,7 @@ end --- Close the chat window. function M.close() - M.chat:close(state.source and state.source.bufnr or nil) + M.chat:close(state.source.bufnr) end --- Toggle the chat window. @@ -822,9 +771,6 @@ function M.ask(prompt, config) '\n' ) - -- Retrieve the selection - local selection = M.get_selection() - async.run(handle_error(config, function() local selected_tools, resolved_resources, resolved_tools, prompt = M.resolve_functions(prompt, config) local selected_model, prompt = M.resolve_model(prompt, config) @@ -883,7 +829,6 @@ function M.ask(prompt, config) local ask_response = client.ask(client, prompt, { headless = config.headless, history = M.chat.messages, - selection = selection, resources = resolved_resources, tools = selected_tools, system_prompt = system_prompt, @@ -937,11 +882,7 @@ function M.stop(reset) if reset then M.chat:clear() vim.diagnostic.reset(vim.api.nvim_create_namespace('copilot-chat-diagnostics')) - - -- Clear the selection - if state.source then - M.set_selection(state.source.bufnr, 0, 0, true) - end + select.set(state.source.bufnr) end if stopped or reset then @@ -1078,7 +1019,7 @@ function M.setup(config) end if M.chat then - M.chat:close(state.source and state.source.bufnr or nil) + M.chat:close(state.source.bufnr) M.chat:delete() end M.chat = require('CopilotChat.ui.chat')(M.config, function(bufnr) @@ -1095,7 +1036,9 @@ function M.setup(config) update_source() end - vim.schedule(update_highlights) + vim.schedule(function() + select.highlight(state.source.bufnr, not (M.config.highlight_selection and M.chat:focused())) + end) end, }) diff --git a/lua/CopilotChat/select.lua b/lua/CopilotChat/select.lua index 8bef366c..1d2b8bcb 100644 --- a/lua/CopilotChat/select.lua +++ b/lua/CopilotChat/select.lua @@ -6,90 +6,70 @@ ---@field filetype string ---@field bufnr number +local constants = require('CopilotChat.constants') +local utils = require('CopilotChat.utils') + local M = {} ---- Select and process current visual selection ---- @param source CopilotChat.source ---- @return CopilotChat.select.Selection|nil -function M.visual(source) - local bufnr = source.bufnr - local start_line = unpack(vim.api.nvim_buf_get_mark(bufnr, '<')) - local finish_line = unpack(vim.api.nvim_buf_get_mark(bufnr, '>')) - if start_line == 0 or finish_line == 0 then - return nil - end - if start_line > finish_line then - start_line, finish_line = finish_line, start_line - end +---@deprecated +function M.visual(_) + vim.deprecate('CopilotChat.select.visual', '#selection', '5.0.0', constants.PLUGIN_NAME) + return nil +end - local ok, lines = pcall(vim.api.nvim_buf_get_lines, bufnr, start_line - 1, finish_line, false) - if not ok then - return nil - end - local lines_content = table.concat(lines, '\n') - if vim.trim(lines_content) == '' then - return nil - end +---@deprecated +function M.buffer(_) + vim.deprecate('CopilotChat.select.buffer', '#selection', '5.0.0', constants.PLUGIN_NAME) + return nil +end - return { - content = lines_content, - filename = vim.api.nvim_buf_get_name(bufnr), - filetype = vim.bo[bufnr].filetype, - start_line = start_line, - end_line = finish_line, - bufnr = bufnr, - } +---@deprecated +function M.line(_) + vim.deprecate('CopilotChat.select.line', '#selection', '5.0.0', constants.PLUGIN_NAME) + return nil end ---- Select and process whole buffer ---- @param source CopilotChat.source ---- @return CopilotChat.select.Selection|nil -function M.buffer(source) - local bufnr = source.bufnr - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - if not lines or #lines == 0 then - return nil +---@deprecated +function M.unnamed(_) + vim.deprecate('CopilotChat.select.unnamed', '#selection', '5.0.0', constants.PLUGIN_NAME) + return nil +end + +--- Highlight selection in target buffer or clear it +---@param bufnr number +---@param clear boolean? +function M.highlight(bufnr, clear) + local selection_ns = vim.api.nvim_create_namespace('copilot-chat-selection') + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + vim.api.nvim_buf_clear_namespace(buf, selection_ns, 0, -1) end - return { - content = table.concat(lines, '\n'), - filename = vim.api.nvim_buf_get_name(bufnr), - filetype = vim.bo[bufnr].filetype, - start_line = 1, - end_line = #lines, - bufnr = bufnr, - } -end + if clear then + return + end ---- Select and process current line ---- @param source CopilotChat.source ---- @return CopilotChat.select.Selection|nil -function M.line(source) - local bufnr = source.bufnr - local winnr = source.winnr - local cursor = vim.api.nvim_win_get_cursor(winnr) - local line = vim.api.nvim_buf_get_lines(bufnr, cursor[1] - 1, cursor[1], false)[1] - if not line then - return nil + local selection = M.get(bufnr) + if not selection then + return end - return { - content = line, - filename = vim.api.nvim_buf_get_name(bufnr), - filetype = vim.bo[bufnr].filetype, - start_line = cursor[1], - end_line = cursor[1], - bufnr = bufnr, - } + vim.api.nvim_buf_set_extmark(selection.bufnr, selection_ns, selection.start_line - 1, 0, { + hl_group = 'CopilotChatSelection', + end_row = selection.end_line, + strict = false, + }) end ---- Select and process contents of unnamed register ("). This register contains last deleted, changed or yanked content. ---- @param source CopilotChat.source ---- @return CopilotChat.select.Selection|nil -function M.unnamed(source) - local bufnr = source.bufnr - local start_line = unpack(vim.api.nvim_buf_get_mark(bufnr, '[')) - local finish_line = unpack(vim.api.nvim_buf_get_mark(bufnr, ']')) +--- Get the selection from the target buffer +---@param bufnr number +---@return CopilotChat.select.Selection? +function M.get(bufnr) + if not utils.buf_valid(bufnr) then + return nil + end + + local start_line = unpack(vim.api.nvim_buf_get_mark(bufnr, '<')) + local finish_line = unpack(vim.api.nvim_buf_get_mark(bufnr, '>')) if start_line == 0 or finish_line == 0 then return nil end @@ -116,4 +96,31 @@ function M.unnamed(source) } end +--- Sets the selection to specific lines in buffer or clears it +---@param bufnr number +---@param winnr number? +---@param start_line number? +---@param end_line number? +function M.set(bufnr, winnr, start_line, end_line) + if not utils.buf_valid(bufnr) then + return + end + + if not start_line or not end_line then + for _, mark in ipairs({ '<', '>', '[', ']' }) do + pcall(vim.api.nvim_buf_del_mark, bufnr, mark) + end + return + end + + pcall(vim.api.nvim_buf_set_mark, bufnr, '<', start_line, 0, {}) + pcall(vim.api.nvim_buf_set_mark, bufnr, '>', end_line, 0, {}) + pcall(vim.api.nvim_buf_set_mark, bufnr, '[', start_line, 0, {}) + pcall(vim.api.nvim_buf_set_mark, bufnr, ']', end_line, 0, {}) + + if winnr and vim.api.nvim_win_is_valid(winnr) then + pcall(vim.api.nvim_win_set_cursor, winnr, { start_line, 0 }) + end +end + return M diff --git a/plugin/CopilotChat.lua b/plugin/CopilotChat.lua index e59ee49e..db83d5b2 100644 --- a/plugin/CopilotChat.lua +++ b/plugin/CopilotChat.lua @@ -14,6 +14,7 @@ local group = vim.api.nvim_create_augroup('CopilotChat', {}) local function setup_highlights() vim.api.nvim_set_hl(0, 'CopilotChatHeader', { link = '@markup.heading.2.markdown', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatSeparator', { link = '@punctuation.special.markdown', default = true }) + vim.api.nvim_set_hl(0, 'CopilotChatSelection', { link = 'Visual', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatStatus', { link = 'DiagnosticHint', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatHelp', { link = 'DiagnosticInfo', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatResource', { link = 'Constant', default = true }) @@ -21,7 +22,6 @@ local function setup_highlights() vim.api.nvim_set_hl(0, 'CopilotChatPrompt', { link = 'Statement', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatModel', { link = 'Type', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatUri', { link = 'Underlined', default = true }) - vim.api.nvim_set_hl(0, 'CopilotChatSelection', { link = 'Visual', default = true }) vim.api.nvim_set_hl(0, 'CopilotChatAnnotation', { link = 'ColorColumn', default = true }) local fg = vim.api.nvim_get_hl(0, { name = 'CopilotChatStatus', link = false }).fg @@ -36,6 +36,18 @@ vim.api.nvim_create_autocmd('ColorScheme', { }) setup_highlights() +vim.api.nvim_create_autocmd('FileType', { + pattern = 'copilot-chat', + group = group, + callback = vim.schedule_wrap(function() + vim.cmd.syntax('match CopilotChatResource "#\\S\\+"') + vim.cmd.syntax('match CopilotChatTool "@\\S\\+"') + vim.cmd.syntax('match CopilotChatPrompt "/\\S\\+"') + vim.cmd.syntax('match CopilotChatModel "\\$\\S\\+"') + vim.cmd.syntax('match CopilotChatUri "##\\S\\+"') + end), +}) + -- Setup commands vim.api.nvim_create_user_command('CopilotChat', function(args) local chat = require('CopilotChat') @@ -79,18 +91,6 @@ vim.api.nvim_create_user_command('CopilotChatReset', function() chat.reset() end, { force = true }) -vim.api.nvim_create_autocmd('FileType', { - pattern = 'copilot-chat', - group = group, - callback = vim.schedule_wrap(function() - vim.cmd.syntax('match CopilotChatResource "#\\S\\+"') - vim.cmd.syntax('match CopilotChatTool "@\\S\\+"') - vim.cmd.syntax('match CopilotChatPrompt "/\\S\\+"') - vim.cmd.syntax('match CopilotChatModel "\\$\\S\\+"') - vim.cmd.syntax('match CopilotChatUri "##\\S\\+"') - end), -}) - local function complete_load() local chat = require('CopilotChat') local options = vim.tbl_map(function(file)