Skip to content
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

[WIP] Insert context using @ commands #174

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4c8cf18
[WIP] Completion menu in markdown files
Odie Jul 26, 2024
ca1d7ed
Now able to provide completion for `@file:path/to/file`
Odie Jul 27, 2024
d97691f
Don't try to configure sources multiple times
Odie Jul 27, 2024
d384fc7
Parse out context request and insert into user message transparently
Odie Jul 27, 2024
09d3037
Misc cleanup to minimize alteration in init.lua
Odie Jul 27, 2024
9a1142b
Extract function definitions from src using treesitter
Odie Jul 29, 2024
8ed36b1
Implements ts function defintion extraction given source filepath
Odie Jul 29, 2024
208478d
[WIP] Trying to get the fnlist into the databae
Odie Jul 31, 2024
75f7496
Able to insert all fn defs for a single src file
Odie Jul 31, 2024
df84cbf
Implements project-wide indexing
Odie Jul 31, 2024
50855a6
Set better log msg levels when building index
Odie Jul 31, 2024
4862407
Verifies we can at least locate all functions for gp.nvim itself
Odie Jul 31, 2024
76243a2
Implements completion behavior for @code
Odie Aug 1, 2024
8173dea
Implements index rebuilding for a single file
Odie Aug 1, 2024
d6f7c71
Adds a metadata (KV store) table to the database
Odie Aug 1, 2024
3217e1c
Refactors scan_directory into Utils.walk_directory
Odie Aug 1, 2024
05f56af
Builds initial fn def index on ChatNew or ChatToggle
Odie Aug 2, 2024
2bd9508
Implements context insertion for @code commands
Odie Aug 2, 2024
40dabee
Renames the sqlite database file
Odie Aug 2, 2024
c247a47
Cleans up debug prints in Context.insert_contexts
Odie Aug 2, 2024
13ed561
Adds python indexing support
Odie Aug 2, 2024
ec80e19
Renames functions_defs table to symbols table.
Odie Aug 2, 2024
999d7ab
Remove stale symbols from the index
Odie Aug 3, 2024
300bd0a
Only attach completeion source to the chat buffer instead of *.md
Odie Aug 3, 2024
52e3ae5
Fixes broken @file when cmd_split was rewritten
Odie Aug 3, 2024
89d5d6e
Adds cmd RpRebuildIndex
Odie Aug 3, 2024
e63c68e
Can now re-index changed files only
Odie Aug 3, 2024
15cc803
Periodically rebuild symbols for stale files
Odie Aug 3, 2024
1951cad
Directory walk now respects .gitignore
Odie Aug 4, 2024
89e1545
Cleans up prints used for debugging
Odie Aug 4, 2024
d8a1651
Adds @include command
Odie Aug 4, 2024
dbc7df8
Fixes bug where .git/* is indexed when .gitignore does not exist
Odie Aug 4, 2024
ab6a3d5
Merge remote-tracking branch 'upstream/main' into feature/insert-context
Odie Aug 4, 2024
bd30d7b
Adds "GpReferenceFunction" to add a @code command for the function under
Odie Aug 5, 2024
24a2cae
Adds "GpReferenceCurrentFile" command to add a @file command the
Odie Aug 9, 2024
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
1 change: 1 addition & 0 deletions after/plugin/gp.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require("gp.completion").register_cmd_source()
45 changes: 45 additions & 0 deletions data/ts_queries/lua.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
;; Matches global and local function declarations
;; function a_fn_name()
;;
;; This will only match on top level functions.
;; Specificadlly, this ignores the local function declarations.
;; We're only doing this because we're requiring the
;; (file, function_name) pair to be unique in the database.
((chunk
(function_declaration
name: (identifier) @name) @body)
(#set! "type" "function"))

;; Matches function declaration using the dot syntax
;; function a_table.a_fn_name()
((chunk
(function_declaration
name: (dot_index_expression) @name) @body)
(#set! "type" "function"))

;; Matches function declaration using the member function syntax
;; function a_table:a_fn_name()
((chunk
(function_declaration
name: (method_index_expression) @name) @body)
(#set! "type" "function"))

;; Matches on:
;; M.some_field = function() end
((chunk
(assignment_statement
(variable_list
name: (dot_index_expression) @name)
(expression_list
value: (function_definition) @body)))
(#set! "type" "function"))

;; Matches on:
;; some_var = function() end
((chunk
(assignment_statement
(variable_list
name: (identifier) @name)
(expression_list
value: (function_definition) @body)))
(#set! "type" "function"))
20 changes: 20 additions & 0 deletions data/ts_queries/python.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
;; Top level function definitions
((module
(function_definition
name: (identifier) @name ) @body
(#not-has-ancestor? @body class_definition))
(#set! "type" "function"))

;; Class member function definitions
((class_definition
name: (identifier) @classname
body: (block
(function_definition
name: (identifier) @name ) @body))
(#set! "type" "class_method"))


;; Class definitions
((class_definition
name: (identifier) @name) @body
(#set! "type" "class"))
226 changes: 226 additions & 0 deletions lua/gp/completion.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
local u = require("gp.utils")
local context = require("gp.context")
local db = require("gp.db")
local cmp = require("cmp")

---@class CompletionSource
---@field db Db
local source = {}

source.src_name = "gp_completion"

---@return CompletionSource
function source.new()
local db_inst = db.open()
return setmetatable({ db = db_inst }, { __index = source })
end

function source.get_trigger_characters()
return { "@", ":", "/" }
end

-- Attaches the completion source to the given `bufnr`
function source.setup_for_chat_buffer(bufnr)
-- Don't attach the completion source if it's already been done
local attached_varname = "gp_source_attached"
if vim.b[attached_varname] then
return
end

-- Attach the completion source
local config = require("cmp.config")
config.set_buffer({
sources = {
{ name = source.src_name },
},
}, bufnr)

-- Set a flag so we don't try to set the source again
vim.b[attached_varname] = true
end

function source.register_cmd_source()
cmp.register_source(source.src_name, source.new())
end

local function extract_cmd(request)
local target = request.context.cursor_before_line
local start = target:match(".*()@")
if start then
return string.sub(target, start, request.offset)
end
end

local function completion_items_for_path(path)
-- The incoming path should either be
-- - A relative path that references a directory
-- - A relative path + partial filename as last component-
-- We need a bit of logic to figure out which directory content to return

--------------------------------------------------------------------
-- Figure out the full path of the directory we're trying to list --
--------------------------------------------------------------------
-- Split the path into component parts
local path_parts = u.path_split(path)
if path[#path] ~= "/" then
table.remove(path_parts)
end

-- Assuming the cwd is the project root directory...
local cwd = vim.fn.getcwd()
local target_dir = u.path_join(cwd, unpack(path_parts))

--------------------------------------------
-- List the items in the target directory --
--------------------------------------------
local handle = vim.loop.fs_scandir(target_dir)
local files = {}

if not handle then
return files
end

while true do
local name, type = vim.loop.fs_scandir_next(handle)
if not name then
break
end

local item_name, item_kind
if type == "file" then
item_kind = cmp.lsp.CompletionItemKind.File
item_name = name
elseif type == "directory" then
item_kind = cmp.lsp.CompletionItemKind.Folder
item_name = name .. "/"
end

table.insert(files, {
label = item_name,
kind = item_kind,
})
end

return files
end

function source:completion_items_for_fn_name(partial_fn_name)
local result = self.db:find_symbol_by_name(partial_fn_name)

local items = {}
if not result then
return items
end

for _, row in ipairs(result) do
local item = {
-- fields meant for nvim-cmp
label = row.name,
labelDetails = {
detail = row.file,
},

-- fields meant for internal use
row = row,
type = "@code",
}

if row.type == "class" then
item.kind = cmp.lsp.CompletionItemKind.Class
elseif row.type == "class_method" then
item.kind = cmp.lsp.CompletionItemKind.Method
else
item.kind = cmp.lsp.CompletionItemKind.Function
end

table.insert(items, item)
end

return items
end

function source.complete(self, request, callback)
local input = string.sub(request.context.cursor_before_line, request.offset - 1)
local cmd = extract_cmd(request)
if not cmd then
return
end

local cmd_parts = context.cmd_split(cmd)

local items = {}
local isIncomplete = true
local cmd_type = cmd_parts[1]

if cmd_type:match("@file") or cmd_type:match("@include") then
-- What's the path we're trying to provide completion for?
local path = cmd_parts[2] or ""

-- List the items in the specified directory
items = completion_items_for_path(path)

-- Say that the entire list has been provided
-- cmp won't call us again to provide an updated list
isIncomplete = false
elseif cmd_type:match("@code") then
local partial_fn_name = cmd_parts[2] or ""

-- When the user confirms completion of an item, we alter the
-- command to look like `@code:path/to/file:fn_name` to uniquely
-- identify a function.
--
-- If the user were to hit backspace to delete through the text,
-- don't process the input until it no longer looks like a path.
if partial_fn_name:match("/") then
return
end

items = self:completion_items_for_fn_name(partial_fn_name)
isIncomplete = false
elseif input:match("^@") then
items = {
{ label = "code", kind = require("cmp").lsp.CompletionItemKind.Keyword },
{ label = "file", kind = require("cmp").lsp.CompletionItemKind.Keyword },
{ label = "include", kind = require("cmp").lsp.CompletionItemKind.Keyword },
}
isIncomplete = false
else
isIncomplete = false
end

local data = { items = items, isIncomplete = isIncomplete }
callback(data)
end

local function search_backwards(buf, pattern)
-- Use nvim_buf_call to execute a Vim command in the buffer context
return vim.api.nvim_buf_call(buf, function()
-- Search backwards for the pattern
local result = vim.fn.searchpos(pattern, "bn")

if result[1] == 0 and result[2] == 0 then
return nil
end
return result
end)
end

function source:execute(item, callback)
if item.type == "@code" then
-- Locate where @command starts and ends
local end_pos = vim.api.nvim_win_get_cursor(0)
local start_pos = search_backwards(0, "@code")

-- Replace it with a custom piece of text and move the cursor to the end of the string
local text = string.format("@code:%s:%s", item.row.file, item.row.name)
vim.api.nvim_buf_set_text(0, start_pos[1] - 1, start_pos[2] - 1, end_pos[1] - 1, end_pos[2], { text })
vim.api.nvim_win_set_cursor(0, { start_pos[1], start_pos[2] - 1 + #text })
end

-- After brief glance at the nvim-cmp source, it appears
-- we should call `callback` to continue the entry item selection
-- confirmation handling chain.
callback()
end

return source
Loading
Loading