From 3b0296358508da1eb258c8716df163dd04fa6449 Mon Sep 17 00:00:00 2001 From: Folke Lemaitre Date: Thu, 25 Sep 2025 10:31:16 +0200 Subject: [PATCH 1/4] feat(copilot): added `copilot-native` extra to setup native inline completions in Neovim --- .../plugins/extras/ai/copilot-native.lua | 49 +++++++++++++++++++ lua/lazyvim/util/lualine.lua | 36 ++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 lua/lazyvim/plugins/extras/ai/copilot-native.lua diff --git a/lua/lazyvim/plugins/extras/ai/copilot-native.lua b/lua/lazyvim/plugins/extras/ai/copilot-native.lua new file mode 100644 index 00000000..8bcb9bfe --- /dev/null +++ b/lua/lazyvim/plugins/extras/ai/copilot-native.lua @@ -0,0 +1,49 @@ +---@diagnostic disable: missing-fields +if lazyvim_docs then + -- Native inline completions don't support being shown as regular completions + vim.g.ai_cmp = false +end + +if LazyVim.has_extra("ai.copilot-native") then + if not vim.lsp.inline_completion then + LazyVim.error("You need Neovim >= 0.12 to use the `ai.copilot-native` extra.") + return {} + end + if LazyVim.has_extra("ai.copilot") then + LazyVim.error("Please disable the `ai.copilot` extra if you want to use `ai.copilot-native`") + return {} + end +end + +vim.g.ai_cmp = false + +return { + desc = "Native Copilot LSP integration. Requires Neovim >= 0.12", + -- copilot-language-server + { + "neovim/nvim-lspconfig", + opts = { + servers = { + copilot = {}, + }, + setup = { + copilot = function(_, opts) + vim.lsp.inline_completion.enable() + LazyVim.cmp.actions.ai_accept = function() + return vim.lsp.inline_completion.get() + end + end, + }, + }, + }, + + -- lualine + { + "nvim-lualine/lualine.nvim", + optional = true, + event = "VeryLazy", + opts = function(_, opts) + table.insert(opts.sections.lualine_x, 2, LazyVim.lualine.lsp("copilot")) + end, + }, +} diff --git a/lua/lazyvim/util/lualine.lua b/lua/lazyvim/util/lualine.lua index a6aa24f3..af4b55f7 100644 --- a/lua/lazyvim/util/lualine.lua +++ b/lua/lazyvim/util/lualine.lua @@ -22,6 +22,42 @@ function M.status(icon, status) } end +--- Status indicator for LSP server activity +---@param server string +---@param opts? { methods?: string[], icon?: string } +function M.lsp(server, opts) + opts = opts or {} + local methods = opts.methods or { "textDocument/completion", "textDocument/inlineCompletion" } + local pending = {} ---@type table + vim.api.nvim_create_autocmd("LspRequest", { + group = vim.api.nvim_create_augroup("lazyvim.lualine.lsp." .. server, { clear = true }), + callback = function(args) + ---@class LspRequestData + ---@field client_id number + ---@field request { bufnr: number, method: string, type: "pending" | "cancel" | "complete" | string } + ---@field request_id number + local data = args.data + local client = vim.lsp.get_client_by_id(data.client_id) + if client and client.name == server and vim.tbl_contains(methods, data.request.method) then + pending[data.request_id] = data.request.type == "pending" or nil + end + end, + }) + return { + function() + return opts.icon + or LazyVim.config.icons.kinds[server:sub(1, 1):upper() .. server:sub(2)] + or LazyVim.config.icons.diagnostics.Hint + end, + cond = function() + return #vim.lsp.get_clients({ name = server, bufnr = 0 }) > 0 + end, + color = function() + return { fg = Snacks.util.color(vim.tbl_isempty(pending) and "Special" or "DiagnosticWarn") } + end, + } +end + ---@param name string ---@param icon? string function M.cmp_source(name, icon) From 9913e1665d783d9c4407633bc33475d403aff433 Mon Sep 17 00:00:00 2001 From: Folke Lemaitre Date: Thu, 25 Sep 2025 11:11:40 +0200 Subject: [PATCH 2/4] feat(copilot-native): better lualine status --- .../plugins/extras/ai/copilot-native.lua | 26 ++++++++++++-- lua/lazyvim/util/lualine.lua | 36 ------------------- 2 files changed, 23 insertions(+), 39 deletions(-) diff --git a/lua/lazyvim/plugins/extras/ai/copilot-native.lua b/lua/lazyvim/plugins/extras/ai/copilot-native.lua index 8bcb9bfe..85cd0d30 100644 --- a/lua/lazyvim/plugins/extras/ai/copilot-native.lua +++ b/lua/lazyvim/plugins/extras/ai/copilot-native.lua @@ -16,6 +16,7 @@ if LazyVim.has_extra("ai.copilot-native") then end vim.g.ai_cmp = false +local status = {} ---@type table return { desc = "Native Copilot LSP integration. Requires Neovim >= 0.12", @@ -24,10 +25,22 @@ return { "neovim/nvim-lspconfig", opts = { servers = { - copilot = {}, + copilot = { + handlers = { + didChangeStatus = function(err, res, ctx) + if err then + return + end + status[ctx.client_id] = res.kind ~= "Normal" and "error" or res.busy and "pending" or "ok" + if res.status == "Error" then + LazyVim.error("Please use `:LspCopilotSignIn` to sign in to Copilot") + end + end, + }, + }, }, setup = { - copilot = function(_, opts) + copilot = function() vim.lsp.inline_completion.enable() LazyVim.cmp.actions.ai_accept = function() return vim.lsp.inline_completion.get() @@ -43,7 +56,14 @@ return { optional = true, event = "VeryLazy", opts = function(_, opts) - table.insert(opts.sections.lualine_x, 2, LazyVim.lualine.lsp("copilot")) + table.insert( + opts.sections.lualine_x, + 2, + LazyVim.lualine.status(LazyVim.config.icons.kinds.Copilot, function() + local clients = vim.lsp.get_clients({ name = "copilot", bufnr = 0 }) + return #clients > 0 and status[clients[1].id] or nil + end) + ) end, }, } diff --git a/lua/lazyvim/util/lualine.lua b/lua/lazyvim/util/lualine.lua index af4b55f7..a6aa24f3 100644 --- a/lua/lazyvim/util/lualine.lua +++ b/lua/lazyvim/util/lualine.lua @@ -22,42 +22,6 @@ function M.status(icon, status) } end ---- Status indicator for LSP server activity ----@param server string ----@param opts? { methods?: string[], icon?: string } -function M.lsp(server, opts) - opts = opts or {} - local methods = opts.methods or { "textDocument/completion", "textDocument/inlineCompletion" } - local pending = {} ---@type table - vim.api.nvim_create_autocmd("LspRequest", { - group = vim.api.nvim_create_augroup("lazyvim.lualine.lsp." .. server, { clear = true }), - callback = function(args) - ---@class LspRequestData - ---@field client_id number - ---@field request { bufnr: number, method: string, type: "pending" | "cancel" | "complete" | string } - ---@field request_id number - local data = args.data - local client = vim.lsp.get_client_by_id(data.client_id) - if client and client.name == server and vim.tbl_contains(methods, data.request.method) then - pending[data.request_id] = data.request.type == "pending" or nil - end - end, - }) - return { - function() - return opts.icon - or LazyVim.config.icons.kinds[server:sub(1, 1):upper() .. server:sub(2)] - or LazyVim.config.icons.diagnostics.Hint - end, - cond = function() - return #vim.lsp.get_clients({ name = server, bufnr = 0 }) > 0 - end, - color = function() - return { fg = Snacks.util.color(vim.tbl_isempty(pending) and "Special" or "DiagnosticWarn") } - end, - } -end - ---@param name string ---@param icon? string function M.cmp_source(name, icon) From 6bd630cec6f905665691d593dc5d0fb3d54f560c Mon Sep 17 00:00:00 2001 From: Folke Lemaitre Date: Thu, 25 Sep 2025 12:30:50 +0200 Subject: [PATCH 3/4] feat(copilot-native): added keymaps to cycle suggestions --- lua/lazyvim/plugins/extras/ai/copilot-native.lua | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lua/lazyvim/plugins/extras/ai/copilot-native.lua b/lua/lazyvim/plugins/extras/ai/copilot-native.lua index 85cd0d30..a4c8005b 100644 --- a/lua/lazyvim/plugins/extras/ai/copilot-native.lua +++ b/lua/lazyvim/plugins/extras/ai/copilot-native.lua @@ -37,6 +37,21 @@ return { end end, }, + -- stylua: ignore + keys = { + { + "", + function() vim.lsp.inline_completion.select({ count = 1 }) end, + desc = "Next Copilot Suggestion", + mode = { "i", "n" }, + }, + { + "", + function() vim.lsp.inline_completion.select({ count = -1 }) end, + desc = "Next Copilot Suggestion", + mode = { "i", "n" }, + }, + }, }, }, setup = { From c83df9e68dd41f5a3f7df5a7048169ee286a7da8 Mon Sep 17 00:00:00 2001 From: Folke Lemaitre Date: Thu, 25 Sep 2025 15:30:28 +0200 Subject: [PATCH 4/4] feat(copilot-native): added experimental support for next edit suggestions. check the docs to enable --- lua/lazyvim/config/keymaps.lua | 1 + .../plugins/extras/ai/copilot-native.lua | 68 ++++++++++++++++++- lua/lazyvim/util/cmp.lua | 1 + 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/lua/lazyvim/config/keymaps.lua b/lua/lazyvim/config/keymaps.lua index 938a2333..752aa602 100644 --- a/lua/lazyvim/config/keymaps.lua +++ b/lua/lazyvim/config/keymaps.lua @@ -49,6 +49,7 @@ map("n", "bD", ":bd", { desc = "Delete Buffer and Window" }) map({ "i", "n", "s" }, "", function() vim.cmd("noh") LazyVim.cmp.actions.snippet_stop() + LazyVim.cmp.actions.ai_stop() return "" end, { expr = true, desc = "Escape and Clear hlsearch" }) diff --git a/lua/lazyvim/plugins/extras/ai/copilot-native.lua b/lua/lazyvim/plugins/extras/ai/copilot-native.lua index a4c8005b..b4a61b20 100644 --- a/lua/lazyvim/plugins/extras/ai/copilot-native.lua +++ b/lua/lazyvim/plugins/extras/ai/copilot-native.lua @@ -2,6 +2,9 @@ if lazyvim_docs then -- Native inline completions don't support being shown as regular completions vim.g.ai_cmp = false + + -- Set to `true` in your `options.lua` to enable experimental support for Next Edit Suggestions + vim.g.copilot_nes = false end if LazyVim.has_extra("ai.copilot-native") then @@ -57,8 +60,41 @@ return { setup = { copilot = function() vim.lsp.inline_completion.enable() + + -- Only trigger NES updates: + -- * when leaving insert mode + -- * when text is changed (in normal mode) + -- * when accepting a next edit suggestion + local nes_update = Snacks.util.debounce(function() + return vim.g.copilot_nes and require("copilot-lsp.nes").request_nes("copilot") + end, { ms = 100 }) + + vim.api.nvim_create_autocmd({ "InsertLeave", "TextChanged" }, { + group = vim.api.nvim_create_augroup("lazyvim.copilot-native.complete", { clear = true }), + callback = nes_update, + }) + + -- Accept inline suggestions or next edits LazyVim.cmp.actions.ai_accept = function() - return vim.lsp.inline_completion.get() + if vim.b.nes_state then + local nes = require("copilot-lsp.nes") + + -- Try to jump to the start of the suggestion edit. + if nes.walk_cursor_start_edit() then + return true + end + + -- apply the pending suggestion and jump to the end of the edit. + if nes.apply_pending_nes() then + nes.walk_cursor_end_edit() + nes_update() -- trigger new nes update after accept + return true + end + end + if vim.lsp.inline_completion.get() then + -- nes_update() -- ensure nes update is triggered after inline completion + return true + end end end, }, @@ -81,4 +117,34 @@ return { ) end, }, + + vim.g.copilot_nes + and { + "copilotlsp-nvim/copilot-lsp", + init = function() + vim.api.nvim_create_autocmd("BufEnter", { + callback = function(ev) + local buf = ev.buf + local client = vim.lsp.get_clients({ name = "copilot", bufnr = buf })[1] + if not client then + return + end + client:notify("textDocument/didFocus", { + textDocument = { + uri = vim.uri_from_bufnr(buf), + }, + }) + end, + }) + + LazyVim.cmp.actions.ai_stop = function() + require("copilot-lsp.nes").clear() + end + end, + keys = { + -- nes is also useful in normal mode + { "", LazyVim.cmp.map({ "ai_accept" }, ""), mode = { "n" }, expr = true }, + }, + } + or nil, } diff --git a/lua/lazyvim/util/cmp.lua b/lua/lazyvim/util/cmp.lua index 0641c892..2637b0d3 100644 --- a/lua/lazyvim/util/cmp.lua +++ b/lua/lazyvim/util/cmp.lua @@ -18,6 +18,7 @@ M.actions = { vim.snippet.stop() end end, + ai_stop = function() end, } ---@param actions string[]