diff --git a/README.md b/README.md index a5cc1e4a..5d156237 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ This plugin is the pure lua replacement for [github/copilot.vim](https://github. While using `copilot.vim`, for the first time since I started using neovim my laptop began to overheat. Additionally, I found the large chunks of ghost text moving around my code, and interfering with my existing cmp ghost text disturbing. As lua is far more efficient and makes things easier to integrate with modern plugins, this repository was created. + ## Install @@ -88,6 +89,7 @@ require('copilot').setup({ }, copilot_node_command = 'node', -- Node.js version must be > 18.x copilot_model = "", -- Current LSP default is gpt-35-turbo, supports gpt-4o-copilot + workspace_folders = {}, server_opts_overrides = {}, }) ``` @@ -111,7 +113,7 @@ require("copilot.panel").refresh() ### suggestion -When `auto_trigger` is `true`, copilot starts suggesting as soon as you enter insert mode. +When `auto_trigger` is `true`, copilot starts suggesting as soon as you enter insert mode. When `auto_trigger` is `false`, use the `next` or `prev` keymap to trigger copilot suggestion. @@ -210,6 +212,22 @@ require("copilot").setup { } ``` +### workspace_folders + +Workspace folders improve Copilot's suggestions. +By default, the root_dir is used as a wokspace_folder. + +Additional folders can be added through the configuration as such: + +```lua +workspace_folders = { + "/home/user/gits", + "/home/user/projects", +} +``` + +They can also be added runtime, using the command `:Copilot workspace add [folderpath]` where `[folderpath]` is the workspace folder. + ## Commands `copilot.lua` defines the `:Copilot` command that can perform various actions. It has completion support, so try it out. diff --git a/lua/copilot/api.lua b/lua/copilot/api.lua index 487cbda7..98a9bdaa 100644 --- a/lua/copilot/api.lua +++ b/lua/copilot/api.lua @@ -227,26 +227,26 @@ mod.handlers = { PanelSolutionsDone = panel.handlers.PanelSolutionsDone, statusNotification = status.handlers.statusNotification, ---@param result copilot_open_url_data - ["copilot/openURL"] = function(_, result) + ["copilot/openURL"] = function(_, result) local success, _ = pcall(vim.ui.open, result.target) if not success then if vim.ui.open ~= nil then vim.api.nvim_echo({ - { "copilot/openURL" }, - { vim.inspect({ _, result }) }, - { "\n", "NONE" }, + { "copilot/openURL" }, + { vim.inspect({ _, result }) }, + { "\n", "NONE" }, }, true, {}) error("Unsupported OS: vim.ui.open exists but failed to execute.") else vim.api.nvim_echo({ - { "copilot/openURL" }, - { vim.inspect({ _, result }) }, - { "\n", "NONE" }, + { "copilot/openURL" }, + { vim.inspect({ _, result }) }, + { "\n", "NONE" }, }, true, {}) error("Unsupported Version: vim.ui.open requires Neovim > 0.10") end end - end + end, } mod.panel = panel diff --git a/lua/copilot/client.lua b/lua/copilot/client.lua index 3669c621..9fd42b40 100644 --- a/lua/copilot/client.lua +++ b/lua/copilot/client.lua @@ -19,7 +19,7 @@ local M = { local function store_client_id(id) if M.id and M.id ~= id then if vim.lsp.get_client_by_id(M.id) then - error("unexpectedly started multiple copilot server") + error("unexpectedly started multiple copilot servers") end end @@ -98,8 +98,22 @@ function M.buf_attach(force) return end - local client_id = lsp_start(M.config) - store_client_id(client_id) + if not M.config then + vim.notify("[Copilot] Cannot attach: configuration not initialized", vim.log.levels.ERROR) + return + end + + local ok, client_id_or_err = pcall(lsp_start, M.config) + if not ok then + vim.notify(string.format("[Copilot] Failed to start LSP client: %s", client_id_or_err), vim.log.levels.ERROR) + return + end + + if client_id_or_err then + store_client_id(client_id_or_err) + else + vim.notify("[Copilot] LSP client failed to start (no client ID returned)", vim.log.levels.ERROR) + end end function M.buf_detach() @@ -166,8 +180,8 @@ local function prepare_client_config(overrides) end local agent_path = vim.api.nvim_get_runtime_file("copilot/dist/language-server.js", false)[1] - if vim.fn.filereadable(agent_path) == 0 then - local err = string.format("Could not find agent.js (bad install?) : %s", agent_path) + if not agent_path or vim.fn.filereadable(agent_path) == 0 then + local err = string.format("Could not find language-server.js (bad install?) : %s", tostring(agent_path)) vim.notify("[Copilot] " .. err, vim.log.levels.ERROR) M.startup_error = err return @@ -179,6 +193,9 @@ local function prepare_client_config(overrides) capabilities.copilot = { openURL = true, } + capabilities.workspace = { + workspaceFolders = true, + } local handlers = { PanelSolution = api.handlers.PanelSolution, @@ -187,13 +204,42 @@ local function prepare_client_config(overrides) ["copilot/openURL"] = api.handlers["copilot/openURL"], } + local root_dir = vim.loop.cwd() + if not root_dir then + root_dir = vim.fn.getcwd() + end + + local workspace_folders = { + --- @type workspace_folder + { + uri = vim.uri_from_fname(root_dir), + -- important to keep root_dir as-is for the name as lsp.lua uses this to check the workspace has not changed + name = root_dir, + }, + } + + local config_workspace_folders = config.get("workspace_folders") --[[@as table]] + + for _, config_workspace_folder in ipairs(config_workspace_folders) do + if config_workspace_folder ~= "" then + table.insert( + workspace_folders, + --- @type workspace_folder + { + uri = vim.uri_from_fname(config_workspace_folder), + name = config_workspace_folder, + } + ) + end + end + return vim.tbl_deep_extend("force", { cmd = { node, agent_path, - '--stdio' + "--stdio", }, - root_dir = vim.loop.cwd(), + root_dir = root_dir, name = "copilot", capabilities = capabilities, get_language_id = function(_, filetype) @@ -205,8 +251,7 @@ local function prepare_client_config(overrides) end vim.schedule(function() - ---@type copilot_set_editor_info_params - local set_editor_info_params = util.get_editor_info() + local set_editor_info_params = util.get_editor_info() --[[@as copilot_set_editor_info_params]] set_editor_info_params.editorConfiguration = util.get_editor_configuration() set_editor_info_params.networkProxy = util.get_network_proxy() local provider_url = config.get("auth_provider_url") @@ -221,7 +266,7 @@ local function prepare_client_config(overrides) M.initialized = true end) end, - on_exit = function(code, _signal, client_id) + on_exit = function(code, _, client_id) if M.id == client_id then vim.schedule(function() M.teardown() @@ -239,6 +284,7 @@ local function prepare_client_config(overrides) init_options = { copilotIntegrationId = "vscode-chat", }, + workspace_folders = workspace_folders, }, overrides) end @@ -252,6 +298,7 @@ function M.setup() is_disabled = false + M.id = nil vim.api.nvim_create_augroup(M.augroup, { clear = true }) vim.api.nvim_create_autocmd("FileType", { @@ -276,4 +323,54 @@ function M.teardown() end end +function M.add_workspace_folder(folder_path) + if type(folder_path) ~= "string" then + vim.notify("[Copilot] Workspace folder path must be a string", vim.log.levels.ERROR) + return false + end + + if vim.fn.isdirectory(folder_path) ~= 1 then + vim.notify("[Copilot] Invalid workspace folder: " .. folder_path, vim.log.levels.ERROR) + return false + end + + -- Normalize path + folder_path = vim.fn.fnamemodify(folder_path, ":p") + + --- @type workspace_folder + local workspace_folder = { + uri = vim.uri_from_fname(folder_path), + name = folder_path, + } + + local workspace_folders = config.get("workspace_folders") --[[@as table]] + if not workspace_folders then + workspace_folders = {} + end + + for _, existing_folder in ipairs(workspace_folders) do + if existing_folder == folder_path then + return + end + end + + table.insert(workspace_folders, { folder_path }) + config.set("workspace_folders", workspace_folders) + + local client = M.get() + if client and client.initialized then + client.notify("workspace/didChangeWorkspaceFolders", { + event = { + added = { workspace_folder }, + removed = {}, + }, + }) + vim.notify("[Copilot] Added workspace folder: " .. folder_path, vim.log.levels.INFO) + else + vim.notify("[Copilot] Workspace folder added for next session: " .. folder_path, vim.log.levels.INFO) + end + + return true +end + return M diff --git a/lua/copilot/command.lua b/lua/copilot/command.lua index 8b5387d4..f51751b8 100644 --- a/lua/copilot/command.lua +++ b/lua/copilot/command.lua @@ -10,7 +10,7 @@ local function node_version_warning(node_version) local line = "Warning: Node.js 16 is approaching end of life and support will be dropped in a future release." if config.get("copilot_node_command") ~= "node" then line = line - .. " 'copilot_node_command' is set to a non-default value. Consider removing it from your configuration." + .. " 'copilot_node_command' is set to a non-default value. Consider removing it from your configuration." end return { line, "MoreMsg" } end diff --git a/lua/copilot/config.lua b/lua/copilot/config.lua index d4bff79e..5cda088a 100644 --- a/lua/copilot/config.lua +++ b/lua/copilot/config.lua @@ -40,6 +40,8 @@ local default_config = { ---@type string|nil auth_provider_url = nil, copilot_node_command = "node", + ---@type string[] + workspace_folders = {}, server_opts_overrides = {}, ---@type string|nil copilot_model = nil, @@ -84,4 +86,14 @@ function mod.get(key) return mod.config end +---@param key string +---@param value any +function mod.set(key, value) + if not mod.config then + error("[Copilot] not initialized") + end + + mod.config[key] = value +end + return mod diff --git a/lua/copilot/init.lua b/lua/copilot/init.lua index 286ec049..6652e3e1 100644 --- a/lua/copilot/init.lua +++ b/lua/copilot/init.lua @@ -2,7 +2,7 @@ local M = { setup_done = false } local config = require("copilot.config") local highlight = require("copilot.highlight") -local create_cmds = function () +local create_cmds = function() vim.api.nvim_create_user_command("CopilotDetach", function() if require("copilot.client").buf_is_attached(0) then vim.deprecate("':CopilotDetach'", "':Copilot detach'", "in future", "copilot.lua") @@ -15,7 +15,7 @@ local create_cmds = function () vim.cmd("Copilot disable") end, {}) - vim.api.nvim_create_user_command("CopilotPanel", function () + vim.api.nvim_create_user_command("CopilotPanel", function() vim.deprecate("':CopilotPanel'", "':Copilot panel'", "in future", "copilot.lua") vim.cmd("Copilot panel") end, {}) diff --git a/lua/copilot/workspace.lua b/lua/copilot/workspace.lua new file mode 100644 index 00000000..c8f722de --- /dev/null +++ b/lua/copilot/workspace.lua @@ -0,0 +1,16 @@ +local mod = {} +---@class workspace_folder +---@field uri string The URI of the workspace folder +---@field name string The name of the workspace folder +function mod.add(opts) + local folder = opts.args + if not folder or folder == "" then + error("Folder is required to add a workspace_folder") + end + + folder = vim.fn.fnamemodify(folder, ":p") + + require("copilot.client").add_workspace_folder(folder) +end + +return mod diff --git a/plugin/copilot.lua b/plugin/copilot.lua index bcc80f65..d786f679 100644 --- a/plugin/copilot.lua +++ b/plugin/copilot.lua @@ -3,6 +3,7 @@ local completion_store = { auth = { "signin", "signout" }, panel = { "accept", "jump_next", "jump_prev", "open", "refresh" }, suggestion = { "accept", "accept_line", "accept_word", "dismiss", "next", "prev", "toggle_auto_trigger" }, + workspace = { "add" }, } vim.api.nvim_create_user_command("Copilot", function(opts) @@ -42,9 +43,15 @@ vim.api.nvim_create_user_command("Copilot", function(opts) return end + local remaining_args = "" + if #params > 2 then + remaining_args = table.concat(params, " ", 3) + end + require("copilot.client").use_client(function() mod[action_name]({ force = opts.bang, + args = remaining_args, }) end) end, {