Skip to content

feat: workspace folders #3 #377

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Mar 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

</details>

## Install
Expand Down Expand Up @@ -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 = {},
})
```
Expand All @@ -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.

Expand Down Expand Up @@ -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.
Expand Down
16 changes: 8 additions & 8 deletions lua/copilot/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
117 changes: 107 additions & 10 deletions lua/copilot/client.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -179,6 +193,9 @@ local function prepare_client_config(overrides)
capabilities.copilot = {
openURL = true,
}
capabilities.workspace = {
workspaceFolders = true,
}

local handlers = {
PanelSolution = api.handlers.PanelSolution,
Expand All @@ -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<string>]]

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)
Expand All @@ -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")
Expand All @@ -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()
Expand All @@ -239,6 +284,7 @@ local function prepare_client_config(overrides)
init_options = {
copilotIntegrationId = "vscode-chat",
},
workspace_folders = workspace_folders,
}, overrides)
end

Expand All @@ -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", {
Expand All @@ -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<string>]]
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
2 changes: 1 addition & 1 deletion lua/copilot/command.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions lua/copilot/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions lua/copilot/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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, {})
Expand Down
16 changes: 16 additions & 0 deletions lua/copilot/workspace.lua
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions plugin/copilot.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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, {
Expand Down