diff --git a/lua/copilot/auth/init.lua b/lua/copilot/auth/init.lua index 82b669e7..b47cd894 100644 --- a/lua/copilot/auth/init.lua +++ b/lua/copilot/auth/init.lua @@ -132,6 +132,7 @@ function M.signout() end) end +---@return string|nil function M.find_config_path() local config = vim.fn.expand("$XDG_CONFIG_HOME") if config and vim.fn.isdirectory(config) > 0 then @@ -151,6 +152,7 @@ function M.find_config_path() logger.error("could not find config path") end +---@return table|nil M.get_creds = function() local filename = M.find_config_path() .. "/github-copilot/apps.json" @@ -180,4 +182,65 @@ function M.info() logger.notify("GitHub Copilot token information: ", info) end +---@class copilot_auth_cache +---@field authenticated boolean|nil +---@field timestamp number + +---@type copilot_auth_cache +local auth_cache = { + authenticated = nil, + timestamp = 0, +} + +---@param authenticated boolean +---@return number +local function get_cache_ttl(authenticated) + if authenticated then + return 300000 -- 5 minutes for true status + else + return 30000 -- 30 seconds for false status + end +end + +---@param client vim.lsp.Client +---@param callback? fun() +local function check_status(client, callback) + api.check_status(client, {}, function(err, status) + auth_cache.timestamp = vim.loop.now() + + if not err and status and status.user then + auth_cache.authenticated = true + else + auth_cache.authenticated = false + end + + if callback then + callback() + end + end) +end + +---@param callback? fun() +function M.is_authenticated(callback) + local current_time = vim.loop.now() + + if auth_cache.authenticated ~= nil then + local ttl = get_cache_ttl(auth_cache.authenticated) + if (current_time - auth_cache.timestamp) < ttl then + return auth_cache.authenticated + end + end + + local client = c.get() + if not client then + auth_cache.authenticated = false + auth_cache.timestamp = current_time + return false + end + + check_status(client, callback) + + return auth_cache.authenticated or false +end + return M diff --git a/lua/copilot/health.lua b/lua/copilot/health.lua new file mode 100644 index 00000000..116c1528 --- /dev/null +++ b/lua/copilot/health.lua @@ -0,0 +1,93 @@ +local M = {} + +local api = require("copilot.api") +local auth = require("copilot.auth") +local c = require("copilot.client") +local config = require("copilot.config") + +local start = vim.health.start or vim.health.report_start +local ok = vim.health.ok or vim.health.report_ok +local warn = vim.health.warn or vim.health.report_warn +local error = vim.health.error or vim.health.report_error +local info = vim.health.info or vim.health.report_info + +function M.check() + start("{copilot.lua}") + info("{copilot.lua} GitHub Copilot plugin for Neovim") + + start("Copilot Dependencies") + + if vim.fn.executable("node") == 1 then + local node_version = vim.fn.system("node --version"):gsub("\n", "") + ok("`node` found: " .. node_version) + else + error("`node` not found in PATH") + info("Install Node.js from https://nodejs.org") + end + + start("Copilot Authentication") + + local github_token = os.getenv("GITHUB_COPILOT_TOKEN") + local gh_token = os.getenv("GH_COPILOT_TOKEN") + if github_token or gh_token then + ok("Environment token found: " .. (github_token and "`GITHUB_COPILOT_TOKEN`" or "`GH_COPILOT_TOKEN`")) + else + info("No environment token set (`GITHUB_COPILOT_TOKEN` or `GH_COPILOT_TOKEN`)") + end + + local config_path = auth.find_config_path() + local creds_ok, creds = pcall(auth.get_creds) + if creds_ok and creds then + ok("Local credentials file found") + info("Location: `" .. (config_path or "unknown") .. "/github-copilot/apps.json`") + else + info("No local credentials file found") + info("Expected location: `" .. (config_path or "unknown") .. "/github-copilot/apps.json`") + info("Run `:Copilot auth` to authenticate") + end + + local client = c.get() + if not client then + error("Copilot LSP client not available") + info("Check that the plugin is properly loaded and configured") + info("Or restart Neovim if the plugin was just installed") + return + end + + start("Copilot LSP Status") + ok("LSP client is available and running") + info("Client ID: " .. tostring(client.id)) + + local lsp_authenticated = auth.is_authenticated() + if lsp_authenticated then + ok("LSP authentication status: authenticated") + else + warn("LSP authentication status: not authenticated") + end + info("For detailed authentication status, run `:Copilot status`") + + start("Copilot Configuration") + local suggestion_config = config.suggestion + if suggestion_config and suggestion_config.enabled ~= false then + ok("Suggestions enabled") + if suggestion_config.auto_trigger ~= false then + info("Auto-trigger: enabled") + else + info("Auto-trigger: disabled (manual trigger only)") + end + else + warn("Suggestions disabled in configuration") + info("Enable with `suggestion = { enabled = true }` in setup()") + end + + local panel_config = config.panel + if panel_config and panel_config.enabled ~= false then + ok("Panel enabled") + info("Panel Keybinding: " .. (panel_config.keymap and panel_config.keymap.open or "")) + else + info("Panel disabled in configuration") + info("Enable with `panel = { enabled = true }` in setup()") + end +end + +return M diff --git a/lua/copilot/suggestion/init.lua b/lua/copilot/suggestion/init.lua index e83ab888..9127df7b 100644 --- a/lua/copilot/suggestion/init.lua +++ b/lua/copilot/suggestion/init.lua @@ -1,4 +1,5 @@ local api = require("copilot.api") +local auth = require("copilot.auth") local c = require("copilot.client") local config = require("copilot.config") local hl_group = require("copilot.highlight").group @@ -484,10 +485,17 @@ local function advance(count, ctx) end local function schedule(ctx) - if not is_enabled() or not c.initialized then + -- in case we need to call the lsp API to know if we are authenticated, + -- we want to retry this method after the authentication check + local is_authenticated = auth.is_authenticated(function() + schedule(ctx) + end) + + if not is_enabled() or not c.initialized or not is_authenticated then clear() return end + logger.trace("suggestion schedule", ctx) if copilot._copilot_timer then diff --git a/tests/test_auth.lua b/tests/test_auth.lua index aa9ccf08..444c91a7 100644 --- a/tests/test_auth.lua +++ b/tests/test_auth.lua @@ -84,4 +84,49 @@ T["auth()"]["auth issue replication"] = function() u.expect_match(messages, ".*Online.*Enabled.*") end +T["auth()"]["is_authenticated when not authed returns false"] = function() + child.configure_copilot() + + local result = child.lua([[ + local auth = require("copilot.auth") + local auth_result = auth.is_authenticated() + return tostring(auth_result) + ]]) + + u.expect_match(result, "false") +end + +T["auth()"]["is_authenticated when authed returns true"] = function() + child.configure_copilot() + child.cmd("Copilot auth") + + child.lua([[ + local messages = "" + local function has_passed() + messages = vim.api.nvim_exec("messages", { output = true }) or "" + return string.find(messages, ".*Authenticated as GitHub user.*") ~= nil + end + + vim.wait(30000, function() + return has_passed() + end, 50) + ]]) + + local result = child.lua([[ + local auth_result = "" + local function has_passed() + auth_result = require("copilot.auth").is_authenticated() or "" + return auth_result == true + end + + vim.wait(30000, function() + return has_passed() + end, 50) + + return tostring(auth_result) + ]]) + + u.expect_match(result, "true") +end + return T