Skip to content

Commit 793107c

Browse files
committed
refactor: migrate to uv async for file operations
Replace plenary async operations with vim.uv async APIs where possible to improve performance and reduce dependencies. This includes: - File operations now use vim.uv.fs_* functions - Simplified files context provider to remove pattern matching - Moved curl helpers to utils module - Cleaned up tiktoken loading process The file operations are still partly synchronous and marked with FIXME comments for future async migration.
1 parent cf6f517 commit 793107c

6 files changed

Lines changed: 124 additions & 105 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ Default contexts are:
275275
- `buffer` - Includes specified buffer in chat context (default current). Supports input.
276276
- `buffers` - Includes all buffers in chat context (default listed). Supports input.
277277
- `file` - Includes content of provided file in chat context. Supports input.
278-
- `files` - Includes all non-hidden filenames in the current workspace in chat context. Supports input.
278+
- `files` - Includes all non-hidden filenames in the current workspace in chat context.
279279
- `git` - Includes current git diff in chat context (default unstaged). Supports input.
280280
- `register` - Includes content of specified register in chat context (default `+`, e.g system clipboard). Supports input.
281281

lua/CopilotChat/config.lua

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -228,15 +228,9 @@ return {
228228
end,
229229
},
230230
files = {
231-
description = 'Includes all non-hidden filenames in the current workspace in chat context. Supports input.',
232-
input = function(callback)
233-
vim.ui.input({
234-
prompt = 'Enter a file pattern> ',
235-
default = '**/*',
236-
}, callback)
237-
end,
238-
resolve = function(input, source)
239-
return context.files(input, source.winnr)
231+
description = 'Includes all non-hidden filenames in the current workspace in chat context.',
232+
resolve = function(_, source)
233+
return context.files(source.winnr)
240234
end,
241235
},
242236
git = {

lua/CopilotChat/context.lua

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -274,19 +274,14 @@ function M.outline(content, filename, ft)
274274
end
275275

276276
--- Get list of all files in workspace
277-
---@param pattern string?
278277
---@param winnr number?
279278
---@return table<CopilotChat.copilot.embed>
280-
function M.files(pattern, winnr)
279+
function M.files(winnr)
281280
local cwd = utils.win_cwd(winnr)
282-
local search = cwd .. '/' .. (pattern or '**/*')
283-
local files = vim.tbl_filter(function(file)
284-
return vim.fn.isdirectory(file) == 0
285-
end, vim.fn.glob(search, false, true))
286-
287-
if #files == 0 then
288-
return {}
289-
end
281+
local files = utils.scan_dir(cwd, {
282+
add_dirs = false,
283+
respect_gitignore = true,
284+
})
290285

291286
local out = {}
292287

@@ -315,17 +310,13 @@ end
315310
---@param filename string
316311
---@return CopilotChat.copilot.embed?
317312
function M.file(filename)
318-
if vim.fn.filereadable(filename) ~= 1 then
319-
return nil
320-
end
321-
322-
local content = vim.fn.readfile(filename)
323-
if not content or #content == 0 then
313+
local content = utils.read_file(filename)
314+
if not content then
324315
return nil
325316
end
326317

327318
return {
328-
content = table.concat(content, '\n'),
319+
content = content,
329320
filename = vim.fn.fnamemodify(filename, ':p:.'),
330321
filetype = vim.filetype.match({ filename = filename }),
331322
}

lua/CopilotChat/copilot.lua

Lines changed: 7 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@
1818
---@field model string?
1919
---@field chunk_size number?
2020

21-
local async = require('plenary.async')
2221
local log = require('plenary.log')
23-
local curl = require('plenary.curl')
2422
local prompts = require('CopilotChat.prompts')
2523
local tiktoken = require('CopilotChat.tiktoken')
2624
local utils = require('CopilotChat.utils')
@@ -49,32 +47,6 @@ local VERSION_HEADERS = {
4947
-- ['x-github-api-version'] = '2023-07-07',
5048
}
5149

52-
local curl_get = async.wrap(function(url, opts, callback)
53-
opts = vim.tbl_deep_extend('force', opts, {
54-
callback = callback,
55-
on_error = function(err)
56-
err = err and err.stderr or vim.inspect(err)
57-
callback(nil, err)
58-
end,
59-
})
60-
curl.get(url, opts)
61-
end, 3)
62-
63-
local curl_post = async.wrap(function(url, opts, callback)
64-
opts = vim.tbl_deep_extend('force', opts, {
65-
callback = callback,
66-
on_error = function(err)
67-
err = err and err.stderr or vim.inspect(err)
68-
callback(nil, err)
69-
end,
70-
})
71-
curl.post(url, opts)
72-
end, 3)
73-
74-
local tiktoken_load = async.wrap(function(tokenizer, callback)
75-
tiktoken.load(tokenizer, callback)
76-
end, 2)
77-
7850
--- Get the github oauth cached token
7951
---@return string|nil
8052
local function get_cached_token()
@@ -384,7 +356,7 @@ function Copilot:authenticate()
384356
['accept'] = 'application/json',
385357
}, VERSION_HEADERS)
386358

387-
local response, err = curl_get(
359+
local response, err = utils.curl_get(
388360
'https://api.github.com/copilot_internal/v2/token',
389361
vim.tbl_extend('force', self.request_args, {
390362
headers = headers,
@@ -427,7 +399,7 @@ function Copilot:fetch_models()
427399
return self.models
428400
end
429401

430-
local response, err = curl_get(
402+
local response, err = utils.curl_get(
431403
'https://api.githubcopilot.com/models',
432404
vim.tbl_extend('force', self.request_args, {
433405
headers = self:authenticate(),
@@ -468,7 +440,7 @@ function Copilot:fetch_agents()
468440
return self.agents
469441
end
470442

471-
local response, err = curl_get(
443+
local response, err = utils.curl_get(
472444
'https://api.githubcopilot.com/agents',
473445
vim.tbl_extend('force', self.request_args, {
474446
headers = self:authenticate(),
@@ -504,7 +476,7 @@ function Copilot:enable_policy(model)
504476
return
505477
end
506478

507-
local response, err = curl_post(
479+
local response, err = utils.curl_post(
508480
'https://api.githubcopilot.com/models/' .. model .. '/policy',
509481
vim.tbl_extend('force', self.request_args, {
510482
headers = self:authenticate(),
@@ -565,7 +537,7 @@ function Copilot:ask(prompt, opts)
565537
local tokenizer = capabilities.tokenizer
566538
log.debug('Max tokens: ' .. max_tokens)
567539
log.debug('Tokenizer: ' .. tokenizer)
568-
tiktoken_load(tokenizer)
540+
tiktoken.load(tokenizer)
569541

570542
local generated_messages = {}
571543
local selection_messages = generate_selection_messages(selection)
@@ -740,7 +712,7 @@ function Copilot:ask(prompt, opts)
740712
args.stream = stream_func
741713
end
742714

743-
local response, err = curl_post(url, args)
715+
local response, err = utils.curl_post(url, args)
744716

745717
if self.current_job ~= job_id then
746718
return nil, nil, nil
@@ -902,7 +874,7 @@ function Copilot:embed(inputs, opts)
902874
for i = 1, #uncached_embeddings, chunk_size do
903875
local chunk = vim.list_slice(uncached_embeddings, i, i + chunk_size - 1)
904876
local body = vim.json.encode(generate_embedding_request(chunk, model))
905-
local response, err = curl_post(
877+
local response, err = utils.curl_post(
906878
'https://api.githubcopilot.com/embeddings',
907879
vim.tbl_extend('force', self.request_args, {
908880
headers = self:authenticate(),

lua/CopilotChat/tiktoken.lua

Lines changed: 36 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,70 @@
1+
local async = require('plenary.async')
12
local curl = require('plenary.curl')
23
local log = require('plenary.log')
4+
local utils = require('CopilotChat.utils')
35
local tiktoken_core = nil
46
local current_tokenizer = nil
5-
6-
local function get_cache_path(fname)
7-
vim.fn.mkdir(tostring(vim.fn.stdpath('cache')), 'p')
8-
return vim.fn.stdpath('cache') .. '/' .. fname
9-
end
10-
11-
local function file_exists(name)
12-
local f = io.open(name, 'r')
13-
if f ~= nil then
14-
io.close(f)
15-
return true
16-
else
17-
return false
18-
end
19-
end
7+
local cache_dir = vim.fn.stdpath('cache')
8+
vim.fn.mkdir(tostring(cache_dir), 'p')
209

2110
--- Load tiktoken data from cache or download it
22-
local function load_tiktoken_data(done, tokenizer)
11+
---@param tokenizer string The tokenizer to load
12+
---@param on_done fun(path: string) The callback to call when the data is loaded
13+
local function load_tiktoken_data(tokenizer, on_done)
2314
local tiktoken_url = 'https://openaipublic.blob.core.windows.net/encodings/'
2415
.. tokenizer
2516
.. '.tiktoken'
26-
local cache_path = get_cache_path(tiktoken_url:match('.+/(.+)'))
17+
local cache_path = cache_dir .. '/' .. tiktoken_url:match('.+/(.+)')
2718

28-
if file_exists(cache_path) then
29-
done(cache_path)
19+
if utils.file_exists(cache_path) then
20+
on_done(cache_path)
3021
return
3122
end
3223

3324
log.info('Downloading tiktoken data from ' .. tiktoken_url)
3425
curl.get(tiktoken_url, {
3526
output = cache_path,
3627
callback = function()
37-
done(cache_path)
28+
on_done(cache_path)
3829
end,
3930
})
4031
end
4132

4233
local M = {}
4334

44-
function M.load(tokenizer, on_done)
35+
--- Load the tiktoken module
36+
---@param tokenizer string The tokenizer to load
37+
M.load = async.wrap(function(tokenizer, callback)
4538
if tokenizer == current_tokenizer then
46-
on_done()
39+
callback()
4740
return
4841
end
4942

5043
local ok, core = pcall(require, 'tiktoken_core')
5144
if not ok then
52-
on_done()
45+
callback()
5346
return
5447
end
5548

56-
vim.schedule(function()
57-
load_tiktoken_data(
58-
vim.schedule_wrap(function(path)
59-
local special_tokens = {}
60-
special_tokens['<|endoftext|>'] = 100257
61-
special_tokens['<|fim_prefix|>'] = 100258
62-
special_tokens['<|fim_middle|>'] = 100259
63-
special_tokens['<|fim_suffix|>'] = 100260
64-
special_tokens['<|endofprompt|>'] = 100276
65-
local pat_str =
66-
"(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+"
67-
core.new(path, special_tokens, pat_str)
68-
tiktoken_core = core
69-
current_tokenizer = tokenizer
70-
on_done()
71-
end),
72-
tokenizer
73-
)
49+
load_tiktoken_data(tokenizer, function(path)
50+
local special_tokens = {}
51+
special_tokens['<|endoftext|>'] = 100257
52+
special_tokens['<|fim_prefix|>'] = 100258
53+
special_tokens['<|fim_middle|>'] = 100259
54+
special_tokens['<|fim_suffix|>'] = 100260
55+
special_tokens['<|endofprompt|>'] = 100276
56+
local pat_str =
57+
"(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}{1,3}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+"
58+
core.new(path, special_tokens, pat_str)
59+
tiktoken_core = core
60+
current_tokenizer = tokenizer
61+
callback()
7462
end)
75-
end
63+
end, 2)
7664

65+
--- Encode a prompt
66+
---@param prompt string The prompt to encode
67+
---@return table?
7768
function M.encode(prompt)
7869
if not tiktoken_core then
7970
return nil
@@ -88,6 +79,9 @@ function M.encode(prompt)
8879
return tiktoken_core.encode(prompt)
8980
end
9081

82+
--- Count the tokens in a prompt
83+
---@param prompt string The prompt to count
84+
---@return number
9185
function M.count(prompt)
9286
if not tiktoken_core then
9387
return math.ceil(#prompt * 0.5) -- Fallback to 1/2 character count

lua/CopilotChat/utils.lua

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
local async = require('plenary.async')
2+
local curl = require('plenary.curl')
3+
local scandir = require('plenary.scandir')
4+
15
local M = {}
26
M.timers = {}
37

@@ -212,7 +216,7 @@ end
212216
---@param winnr number? The buffer number
213217
---@return string
214218
function M.win_cwd(winnr)
215-
if not winnr or not vim.api.nvim_win_is_valid(winnr) then
219+
if not winnr then
216220
return '.'
217221
end
218222

@@ -224,4 +228,68 @@ function M.win_cwd(winnr)
224228
return dir
225229
end
226230

231+
--- Send curl get request
232+
---@param url string The url
233+
---@param opts table The options
234+
M.curl_get = async.wrap(function(url, opts, callback)
235+
curl.get(
236+
url,
237+
vim.tbl_deep_extend('force', opts, {
238+
callback = callback,
239+
on_error = function(err)
240+
err = err and err.stderr or vim.inspect(err)
241+
callback(nil, err)
242+
end,
243+
})
244+
)
245+
end, 3)
246+
247+
--- Send curl post request
248+
---@param url string The url
249+
---@param opts table The options
250+
M.curl_post = async.wrap(function(url, opts, callback)
251+
curl.post(
252+
url,
253+
vim.tbl_deep_extend('force', opts, {
254+
callback = callback,
255+
on_error = function(err)
256+
err = err and err.stderr or vim.inspect(err)
257+
callback(nil, err)
258+
end,
259+
})
260+
)
261+
end, 3)
262+
263+
--- Scan a directory
264+
--- FIXME: Make async
265+
M.scan_dir = scandir.scan_dir
266+
267+
--- Check if a file exists
268+
--- FIXME: Make async
269+
---@param path string The file path
270+
M.file_exists = function(path)
271+
local stat = vim.uv.fs_stat(path)
272+
return stat ~= nil
273+
end
274+
275+
--- Read a file
276+
--- FIXME: Make async
277+
---@param path string The file path
278+
M.read_file = function(path)
279+
local fd = vim.uv.fs_open(path, 'r', 438)
280+
if not fd then
281+
return nil
282+
end
283+
284+
local stat = vim.uv.fs_fstat(fd)
285+
if not stat then
286+
vim.uv.fs_close(fd)
287+
return nil
288+
end
289+
290+
local data = vim.uv.fs_read(fd, stat.size, 0)
291+
vim.uv.fs_close(fd)
292+
return data
293+
end
294+
227295
return M

0 commit comments

Comments
 (0)