migrating the cmp brain

This commit is contained in:
2026-03-24 21:18:06 -04:00
parent 0d83c6ba4d
commit f8ce473877
13 changed files with 252 additions and 386 deletions

View File

@@ -305,8 +305,8 @@ end
---@param base_path string Base path to resolve relative file paths
---@return table[] attached_files List of {path, content} tables
local function read_attached_files(prompt_content, base_path)
local parser = require("codetyper.parser")
local file_refs = parser.extract_file_references(prompt_content)
local extract_file_references = require("codetyper.parser.extract_file_references")
local file_refs = extract_file_references(prompt_content)
local attached = {}
local cwd = vim.fn.getcwd()
local base_dir = vim.fn.fnamemodify(base_path, ":h")
@@ -349,7 +349,10 @@ function M.check_for_closed_prompt()
end
is_processing = true
local parser = require("codetyper.parser")
local has_closing_tag = require("codetyper.parser.has_closing_tag")
local get_last_prompt = require("codetyper.parser.get_last_prompt")
local clean_prompt = require("codetyper.parser.clean_prompt")
local strip_file_references = require("codetyper.parser.strip_file_references")
local bufnr = vim.api.nvim_get_current_buf()
local current_file = vim.fn.expand("%:p")
@@ -373,9 +376,9 @@ function M.check_for_closed_prompt()
local current_line = lines[1]
-- Check if line contains closing tag
if parser.has_closing_tag(current_line, config.patterns.close_tag) then
if has_closing_tag(current_line, config.patterns.close_tag) then
-- Find the complete prompt
local prompt = parser.get_last_prompt(bufnr)
local prompt = get_last_prompt(bufnr)
if prompt and prompt.content and prompt.content ~= "" then
-- Generate unique key for this prompt
local prompt_key = get_prompt_key(bufnr, prompt)
@@ -421,7 +424,7 @@ function M.check_for_closed_prompt()
local attached_files = read_attached_files(prompt.content, current_file)
-- Clean prompt content (strip file references)
local cleaned = parser.clean_prompt(parser.strip_file_references(prompt.content))
local cleaned = clean_prompt(strip_file_references(prompt.content))
-- Check if we're working from a coder file
local is_from_coder_file = utils.is_coder_file(current_file)
@@ -556,7 +559,8 @@ end
---@param current_file string Current file path
---@param skip_processed_check? boolean Skip the processed check (for manual mode)
function M.process_single_prompt(bufnr, prompt, current_file, skip_processed_check)
local parser = require("codetyper.parser")
local clean_prompt = require("codetyper.parser.clean_prompt")
local strip_file_references = require("codetyper.parser.strip_file_references")
local scheduler = require("codetyper.core.scheduler.scheduler")
if not prompt.content or prompt.content == "" then
@@ -606,7 +610,7 @@ function M.process_single_prompt(bufnr, prompt, current_file, skip_processed_che
local attached_files = read_attached_files(prompt.content, current_file)
-- Clean prompt content (strip file references)
local cleaned = parser.clean_prompt(parser.strip_file_references(prompt.content))
local cleaned = clean_prompt(strip_file_references(prompt.content))
-- Resolve scope in target file FIRST (need it to adjust intent)
-- Only resolve scope if NOT from coder file (line numbers don't apply)
@@ -744,7 +748,7 @@ end
--- Check and process all closed prompts in the buffer (works on ANY file)
function M.check_all_prompts()
local parser = require("codetyper.parser")
local find_prompts_in_buffer = require("codetyper.parser.find_prompts_in_buffer")
local bufnr = vim.api.nvim_get_current_buf()
local current_file = vim.fn.expand("%:p")
@@ -754,7 +758,7 @@ function M.check_all_prompts()
end
-- Find all prompts in buffer
local prompts = parser.find_prompts_in_buffer(bufnr)
local prompts = find_prompts_in_buffer(bufnr)
if #prompts == 0 then
return
@@ -777,11 +781,11 @@ end
--- Check for closed prompt with preference check
--- If user hasn't chosen auto/manual mode, ask them first
function M.check_for_closed_prompt_with_preference()
local parser = require("codetyper.parser")
local find_prompts_in_buffer = require("codetyper.parser.find_prompts_in_buffer")
-- First check if there are any prompts to process
local bufnr = vim.api.nvim_get_current_buf()
local prompts = parser.find_prompts_in_buffer(bufnr)
local prompts = find_prompts_in_buffer(bufnr)
if #prompts == 0 then
return
end
@@ -796,11 +800,11 @@ end
--- Check all prompts with preference check
function M.check_all_prompts_with_preference()
local preferences = require("codetyper.config.preferences")
local parser = require("codetyper.parser")
local find_prompts_in_buffer = require("codetyper.parser.find_prompts_in_buffer")
-- First check if there are any prompts to process
local bufnr = vim.api.nvim_get_current_buf()
local prompts = parser.find_prompts_in_buffer(bufnr)
local prompts = find_prompts_in_buffer(bufnr)
if #prompts == 0 then
return
end

View File

@@ -0,0 +1,65 @@
--- Get completion items from brain context
---@param prefix string Current word prefix
---@return table[] items
local function get_brain_completions(prefix)
local items = {}
local ok_brain, brain = pcall(require, "codetyper.brain")
if not ok_brain then
return items
end
-- Check if brain is initialized safely
local is_init = false
if brain.is_initialized then
local ok, result = pcall(brain.is_initialized)
is_init = ok and result
end
if not is_init then
return items
end
-- Query brain for relevant patterns
local ok_query, result = pcall(brain.query, {
query = prefix,
max_results = 10,
types = { "pattern" },
})
if ok_query and result and result.nodes then
for _, node in ipairs(result.nodes) do
if node.c and node.c.s then
local summary = node.c.s
for name in summary:gmatch("functions:%s*([^;]+)") do
for func in name:gmatch("([%w_]+)") do
if func:lower():find(prefix:lower(), 1, true) then
table.insert(items, {
label = func,
kind = 3, -- Function
detail = "[brain]",
documentation = summary,
})
end
end
end
for name in summary:gmatch("classes:%s*([^;]+)") do
for class in name:gmatch("([%w_]+)") do
if class:lower():find(prefix:lower(), 1, true) then
table.insert(items, {
label = class,
kind = 7, -- Class
detail = "[brain]",
documentation = summary,
})
end
end
end
end
end
end
return items
end
return get_brain_completions

View File

@@ -0,0 +1,28 @@
--- Get completion items from current buffer (fallback)
---@param prefix string Current word prefix
---@param bufnr number Buffer number
---@return table[] items
local function get_buffer_completions(prefix, bufnr)
local items = {}
local seen = {}
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local prefix_lower = prefix:lower()
for _, line in ipairs(lines) do
for word in line:gmatch("[%a_][%w_]*") do
if #word >= 3 and word:lower():find(prefix_lower, 1, true) and not seen[word] and word ~= prefix then
seen[word] = true
table.insert(items, {
label = word,
kind = 1, -- Text
detail = "[buffer]",
})
end
end
end
return items
end
return get_buffer_completions

View File

@@ -0,0 +1,30 @@
--- Try to get Copilot suggestion if plugin is installed
---@param prefix string
---@return string|nil suggestion
local function get_copilot_suggestion(prefix)
-- Try copilot.lua suggestion API first
local ok, copilot_suggestion = pcall(require, "copilot.suggestion")
if ok and copilot_suggestion and type(copilot_suggestion.get_suggestion) == "function" then
local ok2, suggestion = pcall(copilot_suggestion.get_suggestion)
if ok2 and suggestion and suggestion ~= "" then
if prefix == "" or suggestion:lower():match(prefix:lower(), 1) then
return suggestion
else
return suggestion
end
end
end
-- Fallback: try older copilot module if present
local ok3, copilot = pcall(require, "copilot")
if ok3 and copilot and type(copilot.get_suggestion) == "function" then
local ok4, suggestion = pcall(copilot.get_suggestion)
if ok4 and suggestion and suggestion ~= "" then
return suggestion
end
end
return nil
end
return get_copilot_suggestion

View File

@@ -0,0 +1,65 @@
--- Get completion items from indexer symbols
---@param prefix string Current word prefix
---@return table[] items
local function get_indexer_completions(prefix)
local items = {}
local ok_indexer, indexer = pcall(require, "codetyper.indexer")
if not ok_indexer then
return items
end
local ok_load, index = pcall(indexer.load_index)
if not ok_load or not index then
return items
end
-- Search symbols
if index.symbols then
for symbol, files in pairs(index.symbols) do
if symbol:lower():find(prefix:lower(), 1, true) then
local files_str = type(files) == "table" and table.concat(files, ", ") or tostring(files)
table.insert(items, {
label = symbol,
kind = 6, -- Variable (generic)
detail = "[index] " .. files_str:sub(1, 30),
documentation = "Symbol found in: " .. files_str,
})
end
end
end
-- Search functions in files
if index.files then
for filepath, file_index in pairs(index.files) do
if file_index and file_index.functions then
for _, func in ipairs(file_index.functions) do
if func.name and func.name:lower():find(prefix:lower(), 1, true) then
table.insert(items, {
label = func.name,
kind = 3, -- Function
detail = "[index] " .. vim.fn.fnamemodify(filepath, ":t"),
documentation = func.docstring or ("Function at line " .. (func.line or "?")),
})
end
end
end
if file_index and file_index.classes then
for _, class in ipairs(file_index.classes) do
if class.name and class.name:lower():find(prefix:lower(), 1, true) then
table.insert(items, {
label = class.name,
kind = 7, -- Class
detail = "[index] " .. vim.fn.fnamemodify(filepath, ":t"),
documentation = class.docstring or ("Class at line " .. (class.line or "?")),
})
end
end
end
end
end
return items
end
return get_indexer_completions

View File

@@ -0,0 +1,7 @@
--- Check if cmp is available
---@return boolean
local function has_cmp()
return pcall(require, "cmp")
end
return has_cmp

View File

@@ -6,226 +6,13 @@
local M = {}
local source = {}
local has_cmp = require("codetyper.adapters.nvim.cmp.has_cmp")
local get_brain_completions = require("codetyper.adapters.nvim.cmp.get_brain_completions")
local get_indexer_completions = require("codetyper.adapters.nvim.cmp.get_indexer_completions")
local get_buffer_completions = require("codetyper.adapters.nvim.cmp.get_buffer_completions")
local get_copilot_suggestion = require("codetyper.adapters.nvim.cmp.get_copilot_suggestion")
--- Check if cmp is available
---@return boolean
local function has_cmp()
return pcall(require, "cmp")
end
--- Get completion items from brain context
---@param prefix string Current word prefix
---@return table[] items
local function get_brain_completions(prefix)
local items = {}
local ok_brain, brain = pcall(require, "codetyper.brain")
if not ok_brain then
return items
end
-- Check if brain is initialized safely
local is_init = false
if brain.is_initialized then
local ok, result = pcall(brain.is_initialized)
is_init = ok and result
end
if not is_init then
return items
end
-- Query brain for relevant patterns
local ok_query, result = pcall(brain.query, {
query = prefix,
max_results = 10,
types = { "pattern" },
})
if ok_query and result and result.nodes then
for _, node in ipairs(result.nodes) do
if node.c and node.c.s then
-- Extract function/class names from summary
local summary = node.c.s
for name in summary:gmatch("functions:%s*([^;]+)") do
for func in name:gmatch("([%w_]+)") do
if func:lower():find(prefix:lower(), 1, true) then
table.insert(items, {
label = func,
kind = 3, -- Function
detail = "[brain]",
documentation = summary,
})
end
end
end
for name in summary:gmatch("classes:%s*([^;]+)") do
for class in name:gmatch("([%w_]+)") do
if class:lower():find(prefix:lower(), 1, true) then
table.insert(items, {
label = class,
kind = 7, -- Class
detail = "[brain]",
documentation = summary,
})
end
end
end
end
end
end
return items
end
--- Get completion items from indexer symbols
---@param prefix string Current word prefix
---@return table[] items
local function get_indexer_completions(prefix)
local items = {}
local ok_indexer, indexer = pcall(require, "codetyper.indexer")
if not ok_indexer then
return items
end
local ok_load, index = pcall(indexer.load_index)
if not ok_load or not index then
return items
end
-- Search symbols
if index.symbols then
for symbol, files in pairs(index.symbols) do
if symbol:lower():find(prefix:lower(), 1, true) then
local files_str = type(files) == "table" and table.concat(files, ", ") or tostring(files)
table.insert(items, {
label = symbol,
kind = 6, -- Variable (generic)
detail = "[index] " .. files_str:sub(1, 30),
documentation = "Symbol found in: " .. files_str,
})
end
end
end
-- Search functions in files
if index.files then
for filepath, file_index in pairs(index.files) do
if file_index and file_index.functions then
for _, func in ipairs(file_index.functions) do
if func.name and func.name:lower():find(prefix:lower(), 1, true) then
table.insert(items, {
label = func.name,
kind = 3, -- Function
detail = "[index] " .. vim.fn.fnamemodify(filepath, ":t"),
documentation = func.docstring or ("Function at line " .. (func.line or "?")),
})
end
end
end
if file_index and file_index.classes then
for _, class in ipairs(file_index.classes) do
if class.name and class.name:lower():find(prefix:lower(), 1, true) then
table.insert(items, {
label = class.name,
kind = 7, -- Class
detail = "[index] " .. vim.fn.fnamemodify(filepath, ":t"),
documentation = class.docstring or ("Class at line " .. (class.line or "?")),
})
end
end
end
end
end
return items
end
--- Get completion items from current buffer (fallback)
---@param prefix string Current word prefix
---@param bufnr number Buffer number
---@return table[] items
local function get_buffer_completions(prefix, bufnr)
local items = {}
local seen = {}
-- Get all lines in buffer
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local prefix_lower = prefix:lower()
for _, line in ipairs(lines) do
-- Extract words that could be identifiers
for word in line:gmatch("[%a_][%w_]*") do
if #word >= 3 and word:lower():find(prefix_lower, 1, true) and not seen[word] and word ~= prefix then
seen[word] = true
table.insert(items, {
label = word,
kind = 1, -- Text
detail = "[buffer]",
})
end
end
end
return items
end
--- Try to get Copilot suggestion if plugin is installed
---@param prefix string
---@return string|nil suggestion
local function get_copilot_suggestion(prefix)
-- Try copilot.lua suggestion API first
local ok, copilot_suggestion = pcall(require, "copilot.suggestion")
if ok and copilot_suggestion and type(copilot_suggestion.get_suggestion) == "function" then
local ok2, suggestion = pcall(copilot_suggestion.get_suggestion)
if ok2 and suggestion and suggestion ~= "" then
-- Only return if suggestion seems to start with prefix (best-effort)
if prefix == "" or suggestion:lower():match(prefix:lower(), 1) then
return suggestion
else
return suggestion
end
end
end
-- Fallback: try older copilot module if present
local ok3, copilot = pcall(require, "copilot")
if ok3 and copilot and type(copilot.get_suggestion) == "function" then
local ok4, suggestion = pcall(copilot.get_suggestion)
if ok4 and suggestion and suggestion ~= "" then
return suggestion
end
end
return nil
end
--- Create new cmp source instance
function source.new()
return setmetatable({}, { __index = source })
end
--- Get source name
function source:get_keyword_pattern()
return [[\k\+]]
end
--- Check if source is available
function source:is_available()
return true
end
--- Get debug name
function source:get_debug_name()
return "codetyper"
end
--- Get trigger characters
function source:get_trigger_characters()
return { ".", ":", "_" }
end
local source = require("codetyper.utils.cmp_source")
--- Complete
---@param params table
@@ -290,9 +77,7 @@ function source:complete(params, callback)
suggestion = res
end
if suggestion and suggestion ~= "" then
-- Truncate suggestion to first line for label display
local first_line = suggestion:match("([^\n]+)") or suggestion
-- Avoid duplicates
if not seen[first_line] then
seen[first_line] = true
table.insert(items, 1, {
@@ -335,7 +120,6 @@ function M.is_registered()
return false
end
-- Try to get registered sources
local config = cmp.get_config()
if config and config.sources then
for _, src in ipairs(config.sources) do

View File

@@ -2,7 +2,8 @@
---
local M = {}
local parser = require("codetyper.parser")
local is_cursor_in_open_tag = require("codetyper.parser.is_cursor_in_open_tag")
local get_file_ref_prefix = require("codetyper.parser.get_file_ref_prefix")
local utils = require("codetyper.support.utils")
--- Get list of files for completion
@@ -110,13 +111,13 @@ end
--- Show file completion popup
function M.show_file_completion()
-- Check if we're in an open prompt tag
local is_inside = parser.is_cursor_in_open_tag()
local is_inside = is_cursor_in_open_tag()
if not is_inside then
return false
end
-- Get the prefix being typed
local prefix = parser.get_file_ref_prefix()
local prefix = get_file_ref_prefix()
if prefix == nil then
return false
end
@@ -160,7 +161,7 @@ function M.setup()
-- Schedule completion popup after the @ is inserted
vim.schedule(function()
-- Check we're in an open tag
local is_inside = parser.is_cursor_in_open_tag()
local is_inside = is_cursor_in_open_tag()
if not is_inside then
return
end

View File

@@ -1,22 +0,0 @@
---@mod codetyper.parser Parser for /@ @/ prompt tags
local logger = require("codetyper.support.logger")
local M = {}
M.find_prompts = require("codetyper.parser.find_prompts")
M.find_prompts_in_buffer = require("codetyper.parser.find_prompts_in_buffer")
M.get_prompt_at_cursor = require("codetyper.parser.get_prompt_at_cursor")
M.get_last_prompt = require("codetyper.parser.get_last_prompt")
M.detect_prompt_type = require("codetyper.parser.detect_prompt_type")
M.clean_prompt = require("codetyper.parser.clean_prompt")
M.has_closing_tag = require("codetyper.parser.has_closing_tag")
M.has_unclosed_prompts = require("codetyper.parser.has_unclosed_prompts")
M.extract_file_references = require("codetyper.parser.extract_file_references")
M.strip_file_references = require("codetyper.parser.strip_file_references")
M.is_cursor_in_open_tag = require("codetyper.parser.is_cursor_in_open_tag")
M.get_file_ref_prefix = require("codetyper.parser.get_file_ref_prefix")
logger.info("parser", "Parser module loaded")
return M

View File

@@ -1,34 +0,0 @@
local logger = require("codetyper.support.logger")
--- Extract the prompt type from content
---@param content string Prompt content
---@return "refactor" | "add" | "document" | "explain" | "generic" Prompt type
local function detect_prompt_type(content)
logger.func_entry("parser", "detect_prompt_type", { content_preview = content:sub(1, 50) })
local lower = content:lower()
if lower:match("refactor") then
logger.debug("parser", "detect_prompt_type: detected 'refactor'")
logger.func_exit("parser", "detect_prompt_type", "refactor")
return "refactor"
elseif lower:match("add") or lower:match("create") or lower:match("implement") then
logger.debug("parser", "detect_prompt_type: detected 'add'")
logger.func_exit("parser", "detect_prompt_type", "add")
return "add"
elseif lower:match("document") or lower:match("comment") or lower:match("jsdoc") then
logger.debug("parser", "detect_prompt_type: detected 'document'")
logger.func_exit("parser", "detect_prompt_type", "document")
return "document"
elseif lower:match("explain") or lower:match("what") or lower:match("how") then
logger.debug("parser", "detect_prompt_type: detected 'explain'")
logger.func_exit("parser", "detect_prompt_type", "explain")
return "explain"
end
logger.debug("parser", "detect_prompt_type: detected 'generic'")
logger.func_exit("parser", "detect_prompt_type", "generic")
return "generic"
end
return detect_prompt_type

View File

@@ -1,56 +0,0 @@
local logger = require("codetyper.support.logger")
local find_prompts_in_buffer = require("codetyper.parser.find_prompts_in_buffer")
--- Get prompt at cursor position
---@param bufnr? number Buffer number (default: current)
---@return CoderPrompt|nil Prompt at cursor or nil
local function get_prompt_at_cursor(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
local cursor = vim.api.nvim_win_get_cursor(0)
local line = cursor[1]
local col = cursor[2] + 1 -- Convert to 1-indexed
logger.func_entry("parser", "get_prompt_at_cursor", {
bufnr = bufnr,
line = line,
col = col,
})
local prompts = find_prompts_in_buffer(bufnr)
logger.debug("parser", "get_prompt_at_cursor: checking " .. #prompts .. " prompts")
for i, prompt in ipairs(prompts) do
logger.debug(
"parser",
"get_prompt_at_cursor: checking prompt " .. i .. " (lines " .. prompt.start_line .. "-" .. prompt.end_line .. ")"
)
if line >= prompt.start_line and line <= prompt.end_line then
logger.debug("parser", "get_prompt_at_cursor: cursor line " .. line .. " is within prompt line range")
if line == prompt.start_line and col < prompt.start_col then
logger.debug(
"parser",
"get_prompt_at_cursor: cursor col " .. col .. " is before prompt start_col " .. prompt.start_col
)
goto continue
end
if line == prompt.end_line and col > prompt.end_col then
logger.debug(
"parser",
"get_prompt_at_cursor: cursor col " .. col .. " is after prompt end_col " .. prompt.end_col
)
goto continue
end
logger.debug("parser", "get_prompt_at_cursor: found prompt at cursor")
logger.func_exit("parser", "get_prompt_at_cursor", "prompt found")
return prompt
end
::continue::
end
logger.debug("parser", "get_prompt_at_cursor: no prompt found at cursor")
logger.func_exit("parser", "get_prompt_at_cursor", nil)
return nil
end
return get_prompt_at_cursor

View File

@@ -1,34 +0,0 @@
local utils = require("codetyper.support.utils")
local logger = require("codetyper.support.logger")
local get_config = require("codetyper.utils.get_config").get_config
--- Check if buffer has any unclosed prompts
---@param bufnr? number Buffer number (default: current)
---@return boolean
local function has_unclosed_prompts(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
logger.func_entry("parser", "has_unclosed_prompts", { bufnr = bufnr })
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local content = table.concat(lines, "\n")
local cfg = get_config()
local escaped_open = utils.escape_pattern(cfg.patterns.open_tag)
local escaped_close = utils.escape_pattern(cfg.patterns.close_tag)
local _, open_count = content:gsub(escaped_open, "")
local _, close_count = content:gsub(escaped_close, "")
local has_unclosed = open_count > close_count
logger.debug(
"parser",
"has_unclosed_prompts: open=" .. open_count .. ", close=" .. close_count .. ", unclosed=" .. tostring(has_unclosed)
)
logger.func_exit("parser", "has_unclosed_prompts", has_unclosed)
return has_unclosed
end
return has_unclosed_prompts

View File

@@ -0,0 +1,28 @@
local source = {}
--- Create new cmp source instance
function source.new()
return setmetatable({}, { __index = source })
end
--- Get source name
function source:get_keyword_pattern()
return [[\k\+]]
end
--- Check if source is available
function source:is_available()
return true
end
--- Get debug name
function source:get_debug_name()
return "codetyper"
end
--- Get trigger characters
function source:get_trigger_characters()
return { ".", ":", "_" }
end
return source