Skip to content

Commit fabeb21

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 <slusnucky@gmail.com>
1 parent 07bcd20 commit fabeb21

File tree

6 files changed

+153
-5
lines changed

6 files changed

+153
-5
lines changed

lua/CopilotChat/client.lua

+18
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,24 @@ function Client:embed(inputs, model)
824824
return results
825825
end
826826

827+
--- Search for the given query
828+
---@param query string: The query to search for
829+
---@param repository string: The repository to search in
830+
---@param model string: The model to use for search
831+
---@return table<CopilotChat.context.embed>
832+
function Client:search(query, repository, model)
833+
local models = self:fetch_models()
834+
835+
local provider_name, search = resolve_provider_function('search', model, models, self.providers)
836+
local headers = self:authenticate(provider_name)
837+
local ok, response = pcall(search, query, repository, headers)
838+
if not ok then
839+
log.warn('Failed to search: ', response)
840+
return {}
841+
end
842+
return response
843+
end
844+
827845
--- Stop the running job
828846
---@return boolean
829847
function Client:stop()

lua/CopilotChat/config/contexts.lua

+7-1
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 {
@@ -160,4 +160,10 @@ return {
160160
return context.quickfix()
161161
end,
162162
},
163+
workspace = {
164+
description = 'Includes all non-hidden files in the current workspace in chat context.',
165+
resolve = function(_, _, prompt, model)
166+
return context.workspace(prompt, model)
167+
end,
168+
},
163169
}

lua/CopilotChat/config/mappings.lua

+2-1
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

+108
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ local utils = require('CopilotChat.utils')
4444
---@field get_agents nil|fun(headers:table):table<CopilotChat.Provider.agent>
4545
---@field get_models nil|fun(headers:table):table<CopilotChat.Provider.model>
4646
---@field embed nil|string|fun(inputs:table<string>, headers:table):table<CopilotChat.Provider.embed>
47+
---@field search nil|string|fun(query:string, repository:string, headers:table):table<CopilotChat.Provider.output>
4748
---@field prepare_input nil|fun(inputs:table<CopilotChat.Provider.input>, opts:CopilotChat.Provider.options):table
4849
---@field prepare_output nil|fun(output:table, opts:CopilotChat.Provider.options):CopilotChat.Provider.output
4950
---@field get_url nil|fun(opts:CopilotChat.Provider.options):string
@@ -101,11 +102,41 @@ local function get_github_token()
101102
error('Failed to find GitHub token')
102103
end
103104

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

107137
M.copilot = {
108138
embed = 'copilot_embeddings',
139+
search = 'copilot_search',
109140

110141
get_headers = function(token)
111142
return {
@@ -279,6 +310,7 @@ M.copilot = {
279310

280311
M.github_models = {
281312
embed = 'copilot_embeddings',
313+
search = 'copilot_search',
282314

283315
get_headers = function(token)
284316
return {
@@ -360,4 +392,80 @@ M.copilot_embeddings = {
360392
end,
361393
}
362394

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

lua/CopilotChat/context.lua

+14
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,20 @@ function M.quickfix()
639639
return out
640640
end
641641

642+
--- Get the content of the current workspace
643+
---@param prompt string
644+
---@param model string
645+
function M.workspace(prompt, model)
646+
local git_remote =
647+
vim.trim(utils.system({ 'git', 'config', '--get', 'remote.origin.url' }).stdout)
648+
local repo_path = git_remote:match('github.com[:/](.+).git$')
649+
if not repo_path then
650+
error('Could not determine GitHub repository from git remote: ' .. git_remote)
651+
end
652+
653+
return client:search(prompt, repo_path, model)
654+
end
655+
642656
--- Filter embeddings based on the query
643657
---@param prompt string
644658
---@param model string

lua/CopilotChat/init.lua

+4-3
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,10 @@ end
220220

221221
--- Resolve the embeddings from the prompt.
222222
---@param prompt string
223+
---@param model string
223224
---@param config CopilotChat.config.shared
224225
---@return table<CopilotChat.context.embed>, string
225-
function M.resolve_embeddings(prompt, config)
226+
function M.resolve_embeddings(prompt, model, config)
226227
local contexts = {}
227228
local function parse_context(prompt_context)
228229
local split = vim.split(prompt_context, ':')
@@ -262,7 +263,7 @@ function M.resolve_embeddings(prompt, config)
262263
for _, context_data in ipairs(contexts) do
263264
local context_value = M.config.contexts[context_data.name]
264265
for _, embedding in
265-
ipairs(context_value.resolve(context_data.input, state.source or {}, prompt))
266+
ipairs(context_value.resolve(context_data.input, state.source or {}, prompt, model))
266267
do
267268
if embedding then
268269
embeddings:set(embedding.filename, embedding)
@@ -648,7 +649,7 @@ function M.ask(prompt, config)
648649
local ok, err = pcall(async.run, function()
649650
local selected_agent, prompt = M.resolve_agent(prompt, config)
650651
local selected_model, prompt = M.resolve_model(prompt, config)
651-
local embeddings, prompt = M.resolve_embeddings(prompt, config)
652+
local embeddings, prompt = M.resolve_embeddings(prompt, selected_model, config)
652653

653654
local has_output = false
654655
local query_ok, filtered_embeddings =

0 commit comments

Comments
 (0)