From f37b42a377ae2f40e5b2f4ae20267d670712ed21 Mon Sep 17 00:00:00 2001 From: Maximilian Wolf <69987866+MaxWolf-01@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:26:27 +0200 Subject: [PATCH] nvim: migrate copilot from plugin to native LSP inline completion Replace copilot.vim with copilot-language-server (LSP) + neovim 0.12's built-in vim.lsp.inline_completion. AcceptWord/AcceptLine implemented via on_accept callback. Blocked on: neovim 0.12.0 landing in nixpkgs-unstable (currently 0.11.6) Co-Authored-By: Claude Opus 4.6 (1M context) --- nix/home/common.nix | 1 + nvim/lsp/copilot.lua | 1 + nvim/lua/lsps/copilot.lua | 117 +++++++++++++++++++++++++++++++++++ nvim/lua/plugins/copilot.lua | 48 -------------- 4 files changed, 119 insertions(+), 48 deletions(-) create mode 100644 nvim/lsp/copilot.lua create mode 100644 nvim/lua/lsps/copilot.lua delete mode 100644 nvim/lua/plugins/copilot.lua diff --git a/nix/home/common.nix b/nix/home/common.nix index 24e81ea..22ef298 100644 --- a/nix/home/common.nix +++ b/nix/home/common.nix @@ -325,6 +325,7 @@ markdown-oxide ncdu neovim + copilot-language-server nerd-fonts.hack nodejs nvd diff --git a/nvim/lsp/copilot.lua b/nvim/lsp/copilot.lua new file mode 100644 index 0000000..11e4063 --- /dev/null +++ b/nvim/lsp/copilot.lua @@ -0,0 +1 @@ +return require("lsps.copilot") diff --git a/nvim/lua/lsps/copilot.lua b/nvim/lua/lsps/copilot.lua new file mode 100644 index 0000000..ecd93e5 --- /dev/null +++ b/nvim/lua/lsps/copilot.lua @@ -0,0 +1,117 @@ +local function sign_in(bufnr, client) + client:request('signIn', vim.empty_dict(), function(err, result) + if err then + vim.notify(err.message, vim.log.levels.ERROR) + return + end + if result.command then + vim.fn.setreg('+', result.userCode) + vim.fn.setreg('*', result.userCode) + local ok = vim.fn.confirm( + 'Copied one-time code to clipboard.\nOpen browser to sign in?', + '&Yes\n&No' + ) + if ok == 1 then + client:exec_cmd(result.command, { bufnr = bufnr }, function(cmd_err, cmd_result) + if cmd_err then + vim.notify(cmd_err.message, vim.log.levels.ERROR) + return + end + if cmd_result.status == 'OK' then + vim.notify('Signed in as ' .. cmd_result.user) + end + end) + end + elseif result.status == 'PromptUserDeviceFlow' then + vim.notify('Enter code ' .. result.userCode .. ' at ' .. result.verificationUri) + elseif result.status == 'AlreadySignedIn' then + vim.notify('Already signed in as ' .. result.user) + end + end) +end + +local function sign_out(_, client) + client:request('signOut', vim.empty_dict(), function(err, result) + if err then + vim.notify(err.message, vim.log.levels.ERROR) + return + end + if result.status == 'NotSignedIn' then + vim.notify('Not signed in.') + end + end) +end + +--- Truncate insert_text via a transform function, skip the server-side accept command. +local function partial_accept(transform) + return function(item) + local text = type(item.insert_text) == 'string' and item.insert_text + or tostring(item.insert_text.value) + local partial = transform(text) + if not partial or partial == '' then return item end + item.insert_text = partial + item.command = nil + return item + end +end + +local accept_word = partial_accept(function(text) + local word = vim.fn.matchstr(text, [[\k\+\s\?]]) + return word ~= '' and word or text:sub(1, 1) +end) + +local accept_line = partial_accept(function(text) + return text:match('^[^\n]*') +end) + +return { + cmd = { 'copilot-language-server', '--stdio' }, + root_markers = { '.git' }, + init_options = { + editorInfo = { + name = 'Neovim', + version = tostring(vim.version()), + }, + editorPluginInfo = { + name = 'Neovim', + version = tostring(vim.version()), + }, + }, + on_attach = function(client, bufnr) + vim.api.nvim_buf_create_user_command(bufnr, 'CopilotSignIn', function() + sign_in(bufnr, client) + end, { desc = 'Sign in to GitHub Copilot' }) + vim.api.nvim_buf_create_user_command(bufnr, 'CopilotSignOut', function() + sign_out(bufnr, client) + end, { desc = 'Sign out of GitHub Copilot' }) + + if not client:supports_method(vim.lsp.protocol.Methods.textDocument_inlineCompletion, bufnr) then + return + end + + vim.lsp.inline_completion.enable(true, { bufnr = bufnr }) + local ic = vim.lsp.inline_completion + local map = vim.keymap.set + + map('i', '', function() ic.get() end, + { buffer = bufnr, desc = 'Copilot: accept suggestion' }) + + map('i', '', function() ic.get({ on_accept = accept_word }) end, + { buffer = bufnr, desc = 'Copilot: accept word' }) + + map('i', '', function() ic.get({ on_accept = accept_line }) end, + { buffer = bufnr, desc = 'Copilot: accept line' }) + + map('i', '', function() ic.select() end, + { buffer = bufnr, desc = 'Copilot: next suggestion' }) + + map('i', '', function() ic.select({ count = -1 }) end, + { buffer = bufnr, desc = 'Copilot: previous suggestion' }) + + map('n', 'cc', function() + local enabled = ic.is_enabled({ bufnr = bufnr }) + ic.enable(not enabled, { bufnr = bufnr }) + vim.notify('Copilot: ' .. (not enabled and 'ON' or 'OFF')) + end, { buffer = bufnr, desc = 'Toggle Copilot' }) + end, +} diff --git a/nvim/lua/plugins/copilot.lua b/nvim/lua/plugins/copilot.lua deleted file mode 100644 index e0b92bc..0000000 --- a/nvim/lua/plugins/copilot.lua +++ /dev/null @@ -1,48 +0,0 @@ -return { - "github/copilot.vim", - config = function() - - -- we'll handle all mappings ourselves - vim.g.copilot_no_tab_map = true - vim.g.copilot_assume_mapped = true - - -- request a suggestion (Alt+Shift to avoid tmux Alt+qweasdf pane switching) - vim.keymap.set("i", "", - "copilot#Suggest()", - { expr = true, silent = true, replace_keycodes = false } - ) - - -- accept next word - vim.keymap.set("i", "", - 'copilot#AcceptWord()', - { expr = true, silent = true, replace_keycodes = false } - ) - - -- accept next line - vim.keymap.set("i", "", - 'copilot#AcceptLine()', - { expr = true, silent = true, replace_keycodes = false } - ) - - -- accept current suggestion - vim.keymap.set("i", "", - 'copilot#Accept("")', - { expr = true, silent = true, replace_keycodes = false } - ) - vim.keymap.set("i", "", "copilot#Dismiss()", { expr = true, silent = true }) - vim.keymap.set("i", "", "copilot#Next()", { expr = true, silent = true }) - vim.keymap.set("i", "", "copilot#Previous()", { expr = true, silent = true }) - - -- toggle Copilot on/off - vim.keymap.set("n", "cc", function() - if vim.b.copilot_enabled == 0 then - vim.cmd("Copilot enable") - vim.notify("Copilot enabled", vim.log.levels.INFO) - else - vim.cmd("Copilot disable") - vim.notify("Copilot disabled", vim.log.levels.INFO) - end - end, { silent = true, desc = "Toggle Copilot" }) - - end, -}