Skip to content

Commit 40360fb

Browse files
committed
feat: Add github workspace command
Supports resolving github code index workspace data and searching in it TODO: Currently this api do not accepts ghu_ github copilot token, and I need to use `gh cli` instead that creates hosts.yml https://github.blog/engineering/the-technology-behind-githubs-new-code-search/ Signed-off-by: Tomas Slusny <[email protected]>
1 parent a3932a1 commit 40360fb

File tree

6 files changed

+153
-5
lines changed

6 files changed

+153
-5
lines changed

lua/CopilotChat/client.lua

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,24 @@ function Client:embed(inputs, model)
809809
return results
810810
end
811811

812+
--- Search for the given query
813+
---@param query string: The query to search for
814+
---@param repository string: The repository to search in
815+
---@param model string: The model to use for search
816+
---@return table<CopilotChat.context.embed>
817+
function Client:search(query, repository, model)
818+
local models = self:fetch_models()
819+
820+
local provider_name, search = resolve_provider_function('search', model, models, self.providers)
821+
local headers = self:authenticate(provider_name)
822+
local ok, response = pcall(search, query, repository, headers)
823+
if not ok then
824+
log.warn('Failed to search: ', response)
825+
return {}
826+
end
827+
return response
828+
end
829+
812830
--- Stop the running job
813831
---@return boolean
814832
function Client:stop()

lua/CopilotChat/config/contexts.lua

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ local utils = require('CopilotChat.utils')
55
---@class CopilotChat.config.context
66
---@field description string?
77
---@field input fun(callback: fun(input: string?), source: CopilotChat.source)?
8-
---@field resolve fun(input: string?, source: CopilotChat.source, prompt: string):table<CopilotChat.context.embed>
8+
---@field resolve fun(input: string?, source: CopilotChat.source, prompt: string, model: string):table<CopilotChat.context.embed>
99

1010
---@type table<string, CopilotChat.config.context>
1111
return {
@@ -173,4 +173,10 @@ return {
173173
return context.quickfix()
174174
end,
175175
},
176+
workspace = {
177+
description = 'Includes all non-hidden files in the current workspace in chat context.',
178+
resolve = function(_, _, prompt, model)
179+
return context.workspace(prompt, model)
180+
end,
181+
},
176182
}

lua/CopilotChat/config/mappings.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,8 @@ return {
409409
async.run(function()
410410
local embeddings = {}
411411
if section and not section.answer then
412-
embeddings = copilot.resolve_embeddings(section.content, chat.config)
412+
local _, selected_model = pcall(copilot.resolve_model, section.content, chat.config)
413+
embeddings = copilot.resolve_embeddings(section.content, selected_model, chat.config)
413414
end
414415

415416
for _, embedding in ipairs(embeddings) do

lua/CopilotChat/config/providers.lua

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ local utils = require('CopilotChat.utils')
4343
---@field get_agents nil|fun(headers:table):table<CopilotChat.Provider.agent>
4444
---@field get_models nil|fun(headers:table):table<CopilotChat.Provider.model>
4545
---@field embed nil|string|fun(inputs:table<string>, headers:table):table<CopilotChat.Provider.embed>
46+
---@field search nil|string|fun(query:string, repository:string, headers:table):table<CopilotChat.Provider.output>
4647
---@field prepare_input nil|fun(inputs:table<CopilotChat.Provider.input>, opts:CopilotChat.Provider.options):table
4748
---@field prepare_output nil|fun(output:table, opts:CopilotChat.Provider.options):CopilotChat.Provider.output
4849
---@field get_url nil|fun(opts:CopilotChat.Provider.options):string
@@ -100,11 +101,41 @@ local function get_github_token()
100101
error('Failed to find GitHub token')
101102
end
102103

104+
local cached_gh_apps_token = nil
105+
106+
--- Get the github apps token (gho_ token)
107+
---@return string
108+
local function get_gh_apps_token()
109+
if cached_gh_apps_token then
110+
return cached_gh_apps_token
111+
end
112+
113+
async.util.scheduler()
114+
115+
local config_path = utils.config_path()
116+
if not config_path then
117+
error('Failed to find config path for GitHub token')
118+
end
119+
120+
local file_path = config_path .. '/gh/hosts.yml'
121+
if vim.fn.filereadable(file_path) == 1 then
122+
local content = table.concat(vim.fn.readfile(file_path), '\n')
123+
local token = content:match('oauth_token:%s*([%w_]+)')
124+
if token then
125+
cached_gh_apps_token = token
126+
return token
127+
end
128+
end
129+
130+
error('Failed to find GitHub token')
131+
end
132+
103133
---@type table<string, CopilotChat.Provider>
104134
local M = {}
105135

106136
M.copilot = {
107137
embed = 'copilot_embeddings',
138+
search = 'copilot_search',
108139

109140
get_headers = function()
110141
local response, err = utils.curl_get('https://api.github.com/copilot_internal/v2/token', {
@@ -271,6 +302,7 @@ M.copilot = {
271302

272303
M.github_models = {
273304
embed = 'copilot_embeddings',
305+
search = 'copilot_search',
274306

275307
get_headers = function()
276308
return {
@@ -350,4 +382,80 @@ M.copilot_embeddings = {
350382
end,
351383
}
352384

385+
M.copilot_search = {
386+
get_headers = M.copilot.get_headers,
387+
388+
get_token = function()
389+
return get_gh_apps_token(), nil
390+
end,
391+
392+
search = function(query, repository, headers)
393+
utils.curl_post(
394+
'https://api.github.com/repos/' .. repository .. '/copilot_internal/embeddings_index',
395+
{
396+
headers = headers,
397+
}
398+
)
399+
400+
local response, err = utils.curl_get(
401+
'https://api.github.com/repos/' .. repository .. '/copilot_internal/embeddings_index',
402+
{
403+
headers = headers,
404+
}
405+
)
406+
407+
if err then
408+
error(err)
409+
end
410+
411+
if response.status ~= 200 then
412+
error('Failed to check search: ' .. tostring(response.status))
413+
end
414+
415+
local body = vim.json.decode(response.body)
416+
417+
if
418+
body.can_index ~= 'ok'
419+
or not body.bm25_search_ok
420+
or not body.lexical_search_ok
421+
or not body.semantic_code_search_ok
422+
or not body.semantic_doc_search_ok
423+
or not body.semantic_indexing_enabled
424+
then
425+
error('Failed to search: ' .. vim.inspect(body))
426+
end
427+
428+
local body = vim.json.encode({
429+
query = query,
430+
scopingQuery = '(repo:' .. repository .. ')',
431+
similarity = 0.766,
432+
limit = 100,
433+
})
434+
435+
local response, err = utils.curl_post('https://api.individual.githubcopilot.com/search/code', {
436+
headers = headers,
437+
body = utils.temp_file(body),
438+
})
439+
440+
if err then
441+
error(err)
442+
end
443+
444+
if response.status ~= 200 then
445+
error('Failed to search: ' .. tostring(response.body))
446+
end
447+
448+
local out = {}
449+
for _, result in ipairs(vim.json.decode(response.body)) do
450+
table.insert(out, {
451+
filename = result.path,
452+
filetype = result.languageName:lower(),
453+
score = result.score,
454+
content = result.contents,
455+
})
456+
end
457+
return out
458+
end,
459+
}
460+
353461
return M

lua/CopilotChat/context.lua

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,20 @@ function M.quickfix()
647647
return out
648648
end
649649

650+
--- Get the content of the current workspace
651+
---@param prompt string
652+
---@param model string
653+
function M.workspace(prompt, model)
654+
local git_remote =
655+
vim.trim(utils.system({ 'git', 'config', '--get', 'remote.origin.url' }).stdout)
656+
local repo_path = git_remote:match('github.com[:/](.+).git$')
657+
if not repo_path then
658+
error('Could not determine GitHub repository from git remote: ' .. git_remote)
659+
end
660+
661+
return client:search(prompt, repo_path, model)
662+
end
663+
650664
--- Filter embeddings based on the query
651665
---@param prompt string
652666
---@param model string

lua/CopilotChat/init.lua

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,10 @@ end
247247

248248
--- Resolve the embeddings from the prompt.
249249
---@param prompt string
250+
---@param model string
250251
---@param config CopilotChat.config.shared
251252
---@return table<CopilotChat.context.embed>, string
252-
function M.resolve_embeddings(prompt, config)
253+
function M.resolve_embeddings(prompt, model, config)
253254
local contexts = {}
254255
local function parse_context(prompt_context)
255256
local split = vim.split(prompt_context, ':')
@@ -289,7 +290,7 @@ function M.resolve_embeddings(prompt, config)
289290
for _, context_data in ipairs(contexts) do
290291
local context_value = M.config.contexts[context_data.name]
291292
for _, embedding in
292-
ipairs(context_value.resolve(context_data.input, state.source or {}, prompt))
293+
ipairs(context_value.resolve(context_data.input, state.source or {}, prompt, model))
293294
do
294295
if embedding then
295296
embeddings:set(embedding.filename, embedding)
@@ -672,7 +673,7 @@ function M.ask(prompt, config)
672673
local ok, err = pcall(async.run, function()
673674
local selected_agent, prompt = M.resolve_agent(prompt, config)
674675
local selected_model, prompt = M.resolve_model(prompt, config)
675-
local embeddings, prompt = M.resolve_embeddings(prompt, config)
676+
local embeddings, prompt = M.resolve_embeddings(prompt, selected_model, config)
676677

677678
local has_output = false
678679
local query_ok, filtered_embeddings =

0 commit comments

Comments
 (0)