Skip to content

Commit e41c376

Browse files
authored
feat: workspace folders (#377)
* feat: initial support for workspaces fixes #375
1 parent acf547e commit e41c376

File tree

8 files changed

+172
-22
lines changed

8 files changed

+172
-22
lines changed

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This plugin is the pure lua replacement for [github/copilot.vim](https://github.
88
While using `copilot.vim`, for the first time since I started using neovim my laptop began to overheat. Additionally,
99
I found the large chunks of ghost text moving around my code, and interfering with my existing cmp ghost text disturbing.
1010
As lua is far more efficient and makes things easier to integrate with modern plugins, this repository was created.
11+
1112
</details>
1213

1314
## Install
@@ -88,6 +89,7 @@ require('copilot').setup({
8889
},
8990
copilot_node_command = 'node', -- Node.js version must be > 18.x
9091
copilot_model = "", -- Current LSP default is gpt-35-turbo, supports gpt-4o-copilot
92+
workspace_folders = {},
9193
server_opts_overrides = {},
9294
})
9395
```
@@ -111,7 +113,7 @@ require("copilot.panel").refresh()
111113

112114
### suggestion
113115

114-
When `auto_trigger` is `true`, copilot starts suggesting as soon as you enter insert mode.
116+
When `auto_trigger` is `true`, copilot starts suggesting as soon as you enter insert mode.
115117

116118
When `auto_trigger` is `false`, use the `next` or `prev` keymap to trigger copilot suggestion.
117119

@@ -210,6 +212,22 @@ require("copilot").setup {
210212
}
211213
```
212214

215+
### workspace_folders
216+
217+
Workspace folders improve Copilot's suggestions.
218+
By default, the root_dir is used as a wokspace_folder.
219+
220+
Additional folders can be added through the configuration as such:
221+
222+
```lua
223+
workspace_folders = {
224+
"/home/user/gits",
225+
"/home/user/projects",
226+
}
227+
```
228+
229+
They can also be added runtime, using the command `:Copilot workspace add [folderpath]` where `[folderpath]` is the workspace folder.
230+
213231
## Commands
214232

215233
`copilot.lua` defines the `:Copilot` command that can perform various actions. It has completion support, so try it out.

lua/copilot/api.lua

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -227,26 +227,26 @@ mod.handlers = {
227227
PanelSolutionsDone = panel.handlers.PanelSolutionsDone,
228228
statusNotification = status.handlers.statusNotification,
229229
---@param result copilot_open_url_data
230-
["copilot/openURL"] = function(_, result)
230+
["copilot/openURL"] = function(_, result)
231231
local success, _ = pcall(vim.ui.open, result.target)
232232
if not success then
233233
if vim.ui.open ~= nil then
234234
vim.api.nvim_echo({
235-
{ "copilot/openURL" },
236-
{ vim.inspect({ _, result }) },
237-
{ "\n", "NONE" },
235+
{ "copilot/openURL" },
236+
{ vim.inspect({ _, result }) },
237+
{ "\n", "NONE" },
238238
}, true, {})
239239
error("Unsupported OS: vim.ui.open exists but failed to execute.")
240240
else
241241
vim.api.nvim_echo({
242-
{ "copilot/openURL" },
243-
{ vim.inspect({ _, result }) },
244-
{ "\n", "NONE" },
242+
{ "copilot/openURL" },
243+
{ vim.inspect({ _, result }) },
244+
{ "\n", "NONE" },
245245
}, true, {})
246246
error("Unsupported Version: vim.ui.open requires Neovim > 0.10")
247247
end
248248
end
249-
end
249+
end,
250250
}
251251

252252
mod.panel = panel

lua/copilot/client.lua

Lines changed: 107 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ local M = {
1919
local function store_client_id(id)
2020
if M.id and M.id ~= id then
2121
if vim.lsp.get_client_by_id(M.id) then
22-
error("unexpectedly started multiple copilot server")
22+
error("unexpectedly started multiple copilot servers")
2323
end
2424
end
2525

@@ -98,8 +98,22 @@ function M.buf_attach(force)
9898
return
9999
end
100100

101-
local client_id = lsp_start(M.config)
102-
store_client_id(client_id)
101+
if not M.config then
102+
vim.notify("[Copilot] Cannot attach: configuration not initialized", vim.log.levels.ERROR)
103+
return
104+
end
105+
106+
local ok, client_id_or_err = pcall(lsp_start, M.config)
107+
if not ok then
108+
vim.notify(string.format("[Copilot] Failed to start LSP client: %s", client_id_or_err), vim.log.levels.ERROR)
109+
return
110+
end
111+
112+
if client_id_or_err then
113+
store_client_id(client_id_or_err)
114+
else
115+
vim.notify("[Copilot] LSP client failed to start (no client ID returned)", vim.log.levels.ERROR)
116+
end
103117
end
104118

105119
function M.buf_detach()
@@ -166,8 +180,8 @@ local function prepare_client_config(overrides)
166180
end
167181

168182
local agent_path = vim.api.nvim_get_runtime_file("copilot/dist/language-server.js", false)[1]
169-
if vim.fn.filereadable(agent_path) == 0 then
170-
local err = string.format("Could not find agent.js (bad install?) : %s", agent_path)
183+
if not agent_path or vim.fn.filereadable(agent_path) == 0 then
184+
local err = string.format("Could not find language-server.js (bad install?) : %s", tostring(agent_path))
171185
vim.notify("[Copilot] " .. err, vim.log.levels.ERROR)
172186
M.startup_error = err
173187
return
@@ -179,6 +193,9 @@ local function prepare_client_config(overrides)
179193
capabilities.copilot = {
180194
openURL = true,
181195
}
196+
capabilities.workspace = {
197+
workspaceFolders = true,
198+
}
182199

183200
local handlers = {
184201
PanelSolution = api.handlers.PanelSolution,
@@ -187,13 +204,42 @@ local function prepare_client_config(overrides)
187204
["copilot/openURL"] = api.handlers["copilot/openURL"],
188205
}
189206

207+
local root_dir = vim.loop.cwd()
208+
if not root_dir then
209+
root_dir = vim.fn.getcwd()
210+
end
211+
212+
local workspace_folders = {
213+
--- @type workspace_folder
214+
{
215+
uri = vim.uri_from_fname(root_dir),
216+
-- important to keep root_dir as-is for the name as lsp.lua uses this to check the workspace has not changed
217+
name = root_dir,
218+
},
219+
}
220+
221+
local config_workspace_folders = config.get("workspace_folders") --[[@as table<string>]]
222+
223+
for _, config_workspace_folder in ipairs(config_workspace_folders) do
224+
if config_workspace_folder ~= "" then
225+
table.insert(
226+
workspace_folders,
227+
--- @type workspace_folder
228+
{
229+
uri = vim.uri_from_fname(config_workspace_folder),
230+
name = config_workspace_folder,
231+
}
232+
)
233+
end
234+
end
235+
190236
return vim.tbl_deep_extend("force", {
191237
cmd = {
192238
node,
193239
agent_path,
194-
'--stdio'
240+
"--stdio",
195241
},
196-
root_dir = vim.loop.cwd(),
242+
root_dir = root_dir,
197243
name = "copilot",
198244
capabilities = capabilities,
199245
get_language_id = function(_, filetype)
@@ -205,8 +251,7 @@ local function prepare_client_config(overrides)
205251
end
206252

207253
vim.schedule(function()
208-
---@type copilot_set_editor_info_params
209-
local set_editor_info_params = util.get_editor_info()
254+
local set_editor_info_params = util.get_editor_info() --[[@as copilot_set_editor_info_params]]
210255
set_editor_info_params.editorConfiguration = util.get_editor_configuration()
211256
set_editor_info_params.networkProxy = util.get_network_proxy()
212257
local provider_url = config.get("auth_provider_url")
@@ -221,7 +266,7 @@ local function prepare_client_config(overrides)
221266
M.initialized = true
222267
end)
223268
end,
224-
on_exit = function(code, _signal, client_id)
269+
on_exit = function(code, _, client_id)
225270
if M.id == client_id then
226271
vim.schedule(function()
227272
M.teardown()
@@ -239,6 +284,7 @@ local function prepare_client_config(overrides)
239284
init_options = {
240285
copilotIntegrationId = "vscode-chat",
241286
},
287+
workspace_folders = workspace_folders,
242288
}, overrides)
243289
end
244290

@@ -252,6 +298,7 @@ function M.setup()
252298

253299
is_disabled = false
254300

301+
M.id = nil
255302
vim.api.nvim_create_augroup(M.augroup, { clear = true })
256303

257304
vim.api.nvim_create_autocmd("FileType", {
@@ -276,4 +323,54 @@ function M.teardown()
276323
end
277324
end
278325

326+
function M.add_workspace_folder(folder_path)
327+
if type(folder_path) ~= "string" then
328+
vim.notify("[Copilot] Workspace folder path must be a string", vim.log.levels.ERROR)
329+
return false
330+
end
331+
332+
if vim.fn.isdirectory(folder_path) ~= 1 then
333+
vim.notify("[Copilot] Invalid workspace folder: " .. folder_path, vim.log.levels.ERROR)
334+
return false
335+
end
336+
337+
-- Normalize path
338+
folder_path = vim.fn.fnamemodify(folder_path, ":p")
339+
340+
--- @type workspace_folder
341+
local workspace_folder = {
342+
uri = vim.uri_from_fname(folder_path),
343+
name = folder_path,
344+
}
345+
346+
local workspace_folders = config.get("workspace_folders") --[[@as table<string>]]
347+
if not workspace_folders then
348+
workspace_folders = {}
349+
end
350+
351+
for _, existing_folder in ipairs(workspace_folders) do
352+
if existing_folder == folder_path then
353+
return
354+
end
355+
end
356+
357+
table.insert(workspace_folders, { folder_path })
358+
config.set("workspace_folders", workspace_folders)
359+
360+
local client = M.get()
361+
if client and client.initialized then
362+
client.notify("workspace/didChangeWorkspaceFolders", {
363+
event = {
364+
added = { workspace_folder },
365+
removed = {},
366+
},
367+
})
368+
vim.notify("[Copilot] Added workspace folder: " .. folder_path, vim.log.levels.INFO)
369+
else
370+
vim.notify("[Copilot] Workspace folder added for next session: " .. folder_path, vim.log.levels.INFO)
371+
end
372+
373+
return true
374+
end
375+
279376
return M

lua/copilot/command.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ local function node_version_warning(node_version)
1010
local line = "Warning: Node.js 16 is approaching end of life and support will be dropped in a future release."
1111
if config.get("copilot_node_command") ~= "node" then
1212
line = line
13-
.. " 'copilot_node_command' is set to a non-default value. Consider removing it from your configuration."
13+
.. " 'copilot_node_command' is set to a non-default value. Consider removing it from your configuration."
1414
end
1515
return { line, "MoreMsg" }
1616
end

lua/copilot/config.lua

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ local default_config = {
4040
---@type string|nil
4141
auth_provider_url = nil,
4242
copilot_node_command = "node",
43+
---@type string[]
44+
workspace_folders = {},
4345
server_opts_overrides = {},
4446
---@type string|nil
4547
copilot_model = nil,
@@ -84,4 +86,14 @@ function mod.get(key)
8486
return mod.config
8587
end
8688

89+
---@param key string
90+
---@param value any
91+
function mod.set(key, value)
92+
if not mod.config then
93+
error("[Copilot] not initialized")
94+
end
95+
96+
mod.config[key] = value
97+
end
98+
8799
return mod

lua/copilot/init.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ local M = { setup_done = false }
22
local config = require("copilot.config")
33
local highlight = require("copilot.highlight")
44

5-
local create_cmds = function ()
5+
local create_cmds = function()
66
vim.api.nvim_create_user_command("CopilotDetach", function()
77
if require("copilot.client").buf_is_attached(0) then
88
vim.deprecate("':CopilotDetach'", "':Copilot detach'", "in future", "copilot.lua")
@@ -15,7 +15,7 @@ local create_cmds = function ()
1515
vim.cmd("Copilot disable")
1616
end, {})
1717

18-
vim.api.nvim_create_user_command("CopilotPanel", function ()
18+
vim.api.nvim_create_user_command("CopilotPanel", function()
1919
vim.deprecate("':CopilotPanel'", "':Copilot panel'", "in future", "copilot.lua")
2020
vim.cmd("Copilot panel")
2121
end, {})

lua/copilot/workspace.lua

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
local mod = {}
2+
---@class workspace_folder
3+
---@field uri string The URI of the workspace folder
4+
---@field name string The name of the workspace folder
5+
function mod.add(opts)
6+
local folder = opts.args
7+
if not folder or folder == "" then
8+
error("Folder is required to add a workspace_folder")
9+
end
10+
11+
folder = vim.fn.fnamemodify(folder, ":p")
12+
13+
require("copilot.client").add_workspace_folder(folder)
14+
end
15+
16+
return mod

plugin/copilot.lua

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ local completion_store = {
33
auth = { "signin", "signout" },
44
panel = { "accept", "jump_next", "jump_prev", "open", "refresh" },
55
suggestion = { "accept", "accept_line", "accept_word", "dismiss", "next", "prev", "toggle_auto_trigger" },
6+
workspace = { "add" },
67
}
78

89
vim.api.nvim_create_user_command("Copilot", function(opts)
@@ -42,9 +43,15 @@ vim.api.nvim_create_user_command("Copilot", function(opts)
4243
return
4344
end
4445

46+
local remaining_args = ""
47+
if #params > 2 then
48+
remaining_args = table.concat(params, " ", 3)
49+
end
50+
4551
require("copilot.client").use_client(function()
4652
mod[action_name]({
4753
force = opts.bang,
54+
args = remaining_args,
4855
})
4956
end)
5057
end, {

0 commit comments

Comments
 (0)