Fixing the old configuration

This commit is contained in:
2026-03-18 21:56:45 -04:00
parent f6266c7d94
commit 9f229b26c9
51 changed files with 791 additions and 6549 deletions

View File

@@ -154,7 +154,7 @@ function M.setup()
-- Auto-set filetype for coder files based on extension
vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, {
group = group,
pattern = "*.coder.*",
pattern = "*.codetyper/*",
callback = function()
M.set_coder_filetype()
end,
@@ -164,7 +164,7 @@ function M.setup()
-- Auto-open split view when opening a coder file directly (e.g., from nvim-tree)
vim.api.nvim_create_autocmd("BufEnter", {
group = group,
pattern = "*.coder.*",
pattern = "*.codetyper/*",
callback = function()
-- Delay slightly to ensure buffer is fully loaded
vim.defer_fn(function()
@@ -177,7 +177,7 @@ function M.setup()
-- Cleanup on buffer close
vim.api.nvim_create_autocmd("BufWipeout", {
group = group,
pattern = "*.coder.*",
pattern = "*.codetyper/*",
callback = function(ev)
local window = require("codetyper.adapters.nvim.windows")
if window.is_open() then
@@ -203,11 +203,11 @@ function M.setup()
callback = function(ev)
-- Skip coder files and tree.log itself
local filepath = ev.file or vim.fn.expand("%:p")
if filepath:match("%.coder%.") or filepath:match("tree%.log$") then
if filepath:match("%.codetyper%.") or filepath:match("tree%.log$") then
return
end
-- Skip non-project files
if filepath:match("node_modules") or filepath:match("%.git/") or filepath:match("%.coder/") then
if filepath:match("node_modules") or filepath:match("%.git/") or filepath:match("%.codetyper/") then
return
end
-- Schedule tree update with debounce
@@ -237,7 +237,7 @@ function M.setup()
callback = function(ev)
local filepath = ev.file or ""
-- Skip special buffers and coder files
if filepath == "" or filepath:match("%.coder%.") or filepath:match("tree%.log$") then
if filepath == "" or filepath:match("%.codetyper%.") or filepath:match("tree%.log$") then
return
end
schedule_tree_update()
@@ -286,23 +286,6 @@ function M.setup()
thinking.setup()
end
--- Get config with fallback defaults
local function get_config_safe()
local codetyper = require("codetyper")
local config = codetyper.get_config()
-- Return defaults if not initialized
if not config or not config.patterns then
return {
patterns = {
open_tag = "/@",
close_tag = "@/",
file_pattern = "*.coder.*",
},
}
end
return config
end
--- Create extmarks for injection range so position survives user edits (99-style).
---@param target_bufnr number Target buffer (where code will be injected)
---@param range { start_line: number, end_line: number } Range to mark (1-based)
@@ -383,7 +366,6 @@ function M.check_for_closed_prompt()
end
is_processing = true
local config = get_config_safe()
local parser = require("codetyper.parser")
local bufnr = vim.api.nvim_get_current_buf()
@@ -793,7 +775,6 @@ 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 preferences = require("codetyper.config.preferences")
local parser = require("codetyper.parser")
-- First check if there are any prompts to process
@@ -803,27 +784,6 @@ function M.check_for_closed_prompt_with_preference()
return
end
-- Check user preference
local auto_process = preferences.is_auto_process_enabled()
if auto_process == nil then
-- Not yet decided - ask the user (but only once per session)
if not asking_preference then
asking_preference = true
preferences.ask_auto_process_preference(function(enabled)
asking_preference = false
if enabled then
-- User chose automatic - process now
M.check_for_closed_prompt()
else
-- User chose manual - show hint
utils.notify("Use :CoderProcess to process prompt tags manually", vim.log.levels.INFO)
end
end)
end
return
end
if auto_process then
-- Automatic mode - process prompts
M.check_for_closed_prompt()
@@ -857,27 +817,6 @@ function M.check_all_prompts_with_preference()
return
end
-- Check user preference
local auto_process = preferences.is_auto_process_enabled()
if auto_process == nil then
-- Not yet decided - ask the user (but only once per session)
if not asking_preference then
asking_preference = true
preferences.ask_auto_process_preference(function(enabled)
asking_preference = false
if enabled then
-- User chose automatic - process now
M.check_all_prompts()
else
-- User chose manual - show hint
utils.notify("Use :CoderProcess to process prompt tags manually", vim.log.levels.INFO)
end
end)
end
return
end
if auto_process then
-- Automatic mode - process prompts
M.check_all_prompts()
@@ -1006,8 +945,8 @@ end
function M.set_coder_filetype()
local filepath = vim.fn.expand("%:p")
-- Extract the actual extension (e.g., index.coder.ts -> ts)
local ext = filepath:match("%.coder%.(%w+)$")
-- Extract the actual extension (e.g., index.codetyper/ts -> ts)
local ext = filepath:match("%.codetyper%.(%w+)$")
if ext then
-- Map extension to filetype
@@ -1196,7 +1135,7 @@ end
--- Directories to ignore for coder file creation
local ignored_directories = {
".git",
".coder",
".codetyper",
".claude",
".vscode",
".idea",
@@ -1432,7 +1371,6 @@ function M.auto_index_file(bufnr)
comment_prefix .. " This file describes the business logic and behavior of " .. filename
)
table.insert(pseudo_code, comment_prefix .. " Edit this pseudo-code to guide code generation.")
table.insert(pseudo_code, comment_prefix .. " Use /@ @/ tags for specific generation requests.")
table.insert(pseudo_code, comment_prefix .. "")
-- Module purpose
@@ -1534,12 +1472,11 @@ function M.auto_index_file(bufnr)
table.insert(pseudo_code, comment_prefix .. " TODO: Describe what you want to build in this file")
table.insert(pseudo_code, comment_prefix .. "")
table.insert(pseudo_code, comment_prefix .. " Example pseudo-code:")
table.insert(pseudo_code, comment_prefix .. " /@")
table.insert(pseudo_code, comment_prefix .. " Create a module that:")
table.insert(pseudo_code, comment_prefix .. " 1. Exports a main function")
table.insert(pseudo_code, comment_prefix .. " 2. Handles errors gracefully")
table.insert(pseudo_code, comment_prefix .. " 3. Returns structured data")
table.insert(pseudo_code, comment_prefix .. " @/")
table.insert(pseudo_code, comment_prefix .. "")
end
@@ -1568,7 +1505,6 @@ function M.auto_index_file(bufnr)
comment_prefix
.. " ═══════════════════════════════════════════════════════════"
)
table.insert(pseudo_code, comment_prefix .. " Use /@ @/ tags below to request code generation:")
table.insert(
pseudo_code,
comment_prefix

View File

@@ -1,7 +1,5 @@
---@mod codetyper.logs_panel Standalone logs panel for code generation
---
--- Shows real-time logs when generating code via /@ @/ prompts.
local M = {}
local logs = require("codetyper.adapters.nvim.ui.logs")
@@ -17,13 +15,13 @@ local queue = require("codetyper.core.events.queue")
---@field queue_listener_id number|nil Listener ID for queue
local state = {
buf = nil,
win = nil,
queue_buf = nil,
queue_win = nil,
is_open = false,
listener_id = nil,
queue_listener_id = nil,
buf = nil,
win = nil,
queue_buf = nil,
queue_win = nil,
is_open = false,
listener_id = nil,
queue_listener_id = nil,
}
--- Namespace for highlights
@@ -37,346 +35,346 @@ local QUEUE_HEIGHT = 8
--- Add a log entry to the buffer
---@param entry table Log entry
local function add_log_entry(entry)
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
return
end
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
return
end
vim.schedule(function()
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
return
end
vim.schedule(function()
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
return
end
-- Handle clear event
if entry.level == "clear" then
vim.bo[state.buf].modifiable = true
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, {
"Generation Logs",
string.rep("", LOGS_WIDTH - 2),
"",
})
vim.bo[state.buf].modifiable = false
return
end
-- Handle clear event
if entry.level == "clear" then
vim.bo[state.buf].modifiable = true
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, {
"Generation Logs",
string.rep("", LOGS_WIDTH - 2),
"",
})
vim.bo[state.buf].modifiable = false
return
end
vim.bo[state.buf].modifiable = true
vim.bo[state.buf].modifiable = true
local formatted = logs.format_entry(entry)
local formatted_lines = vim.split(formatted, "\n", { plain = true })
local line_count = vim.api.nvim_buf_line_count(state.buf)
local formatted = logs.format_entry(entry)
local formatted_lines = vim.split(formatted, "\n", { plain = true })
local line_count = vim.api.nvim_buf_line_count(state.buf)
vim.api.nvim_buf_set_lines(state.buf, -1, -1, false, formatted_lines)
vim.api.nvim_buf_set_lines(state.buf, -1, -1, false, formatted_lines)
-- Apply highlighting based on level
local hl_map = {
info = "DiagnosticInfo",
debug = "Comment",
request = "DiagnosticWarn",
response = "DiagnosticOk",
tool = "DiagnosticHint",
error = "DiagnosticError",
}
-- Apply highlighting based on level
local hl_map = {
info = "DiagnosticInfo",
debug = "Comment",
request = "DiagnosticWarn",
response = "DiagnosticOk",
tool = "DiagnosticHint",
error = "DiagnosticError",
}
local hl = hl_map[entry.level] or "Normal"
for i = 0, #formatted_lines - 1 do
vim.api.nvim_buf_add_highlight(state.buf, ns_logs, hl, line_count + i, 0, -1)
end
local hl = hl_map[entry.level] or "Normal"
for i = 0, #formatted_lines - 1 do
vim.api.nvim_buf_add_highlight(state.buf, ns_logs, hl, line_count + i, 0, -1)
end
vim.bo[state.buf].modifiable = false
vim.bo[state.buf].modifiable = false
-- Auto-scroll logs
if state.win and vim.api.nvim_win_is_valid(state.win) then
local new_count = vim.api.nvim_buf_line_count(state.buf)
pcall(vim.api.nvim_win_set_cursor, state.win, { new_count, 0 })
end
end)
-- Auto-scroll logs
if state.win and vim.api.nvim_win_is_valid(state.win) then
local new_count = vim.api.nvim_buf_line_count(state.buf)
pcall(vim.api.nvim_win_set_cursor, state.win, { new_count, 0 })
end
end)
end
--- Update the title with token counts
local function update_title()
if not state.win or not vim.api.nvim_win_is_valid(state.win) then
return
end
if not state.win or not vim.api.nvim_win_is_valid(state.win) then
return
end
local prompt_tokens, response_tokens = logs.get_token_totals()
local provider, model = logs.get_provider_info()
local prompt_tokens, response_tokens = logs.get_token_totals()
local provider, model = logs.get_provider_info()
if provider and state.buf and vim.api.nvim_buf_is_valid(state.buf) then
vim.bo[state.buf].modifiable = true
local title = string.format("%s | %d/%d tokens", (provider or ""):upper(), prompt_tokens, response_tokens)
vim.api.nvim_buf_set_lines(state.buf, 0, 1, false, { title })
vim.bo[state.buf].modifiable = false
end
if provider and state.buf and vim.api.nvim_buf_is_valid(state.buf) then
vim.bo[state.buf].modifiable = true
local title = string.format("%s | %d/%d tokens", (provider or ""):upper(), prompt_tokens, response_tokens)
vim.api.nvim_buf_set_lines(state.buf, 0, 1, false, { title })
vim.bo[state.buf].modifiable = false
end
end
--- Update the queue display
local function update_queue_display()
if not state.queue_buf or not vim.api.nvim_buf_is_valid(state.queue_buf) then
return
end
if not state.queue_buf or not vim.api.nvim_buf_is_valid(state.queue_buf) then
return
end
vim.schedule(function()
if not state.queue_buf or not vim.api.nvim_buf_is_valid(state.queue_buf) then
return
end
vim.schedule(function()
if not state.queue_buf or not vim.api.nvim_buf_is_valid(state.queue_buf) then
return
end
vim.bo[state.queue_buf].modifiable = true
vim.bo[state.queue_buf].modifiable = true
local lines = {
"Queue",
string.rep("", LOGS_WIDTH - 2),
}
local lines = {
"Queue",
string.rep("", LOGS_WIDTH - 2),
}
-- Get all events (pending and processing)
local pending = queue.get_pending()
local processing = queue.get_processing()
-- Get all events (pending and processing)
local pending = queue.get_pending()
local processing = queue.get_processing()
-- Add processing events first
for _, event in ipairs(processing) do
local filename = vim.fn.fnamemodify(event.target_path or "", ":t")
local line_num = event.range and event.range.start_line or 0
local prompt_preview = (event.prompt_content or ""):sub(1, 25):gsub("\n", " ")
if #(event.prompt_content or "") > 25 then
prompt_preview = prompt_preview .. "..."
end
table.insert(lines, string.format("▶ %s:%d %s", filename, line_num, prompt_preview))
end
-- Add processing events first
for _, event in ipairs(processing) do
local filename = vim.fn.fnamemodify(event.target_path or "", ":t")
local line_num = event.range and event.range.start_line or 0
local prompt_preview = (event.prompt_content or ""):sub(1, 25):gsub("\n", " ")
if #(event.prompt_content or "") > 25 then
prompt_preview = prompt_preview .. "..."
end
table.insert(lines, string.format("▶ %s:%d %s", filename, line_num, prompt_preview))
end
-- Add pending events
for _, event in ipairs(pending) do
local filename = vim.fn.fnamemodify(event.target_path or "", ":t")
local line_num = event.range and event.range.start_line or 0
local prompt_preview = (event.prompt_content or ""):sub(1, 25):gsub("\n", " ")
if #(event.prompt_content or "") > 25 then
prompt_preview = prompt_preview .. "..."
end
table.insert(lines, string.format("○ %s:%d %s", filename, line_num, prompt_preview))
end
-- Add pending events
for _, event in ipairs(pending) do
local filename = vim.fn.fnamemodify(event.target_path or "", ":t")
local line_num = event.range and event.range.start_line or 0
local prompt_preview = (event.prompt_content or ""):sub(1, 25):gsub("\n", " ")
if #(event.prompt_content or "") > 25 then
prompt_preview = prompt_preview .. "..."
end
table.insert(lines, string.format("○ %s:%d %s", filename, line_num, prompt_preview))
end
if #pending == 0 and #processing == 0 then
table.insert(lines, " (empty)")
end
if #pending == 0 and #processing == 0 then
table.insert(lines, " (empty)")
end
vim.api.nvim_buf_set_lines(state.queue_buf, 0, -1, false, lines)
vim.api.nvim_buf_set_lines(state.queue_buf, 0, -1, false, lines)
-- Apply highlights
vim.api.nvim_buf_clear_namespace(state.queue_buf, ns_queue, 0, -1)
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "Title", 0, 0, -1)
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "Comment", 1, 0, -1)
-- Apply highlights
vim.api.nvim_buf_clear_namespace(state.queue_buf, ns_queue, 0, -1)
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "Title", 0, 0, -1)
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "Comment", 1, 0, -1)
local line_idx = 2
for _ = 1, #processing do
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "DiagnosticWarn", line_idx, 0, 1)
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "String", line_idx, 2, -1)
line_idx = line_idx + 1
end
for _ = 1, #pending do
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "Comment", line_idx, 0, 1)
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "Normal", line_idx, 2, -1)
line_idx = line_idx + 1
end
local line_idx = 2
for _ = 1, #processing do
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "DiagnosticWarn", line_idx, 0, 1)
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "String", line_idx, 2, -1)
line_idx = line_idx + 1
end
for _ = 1, #pending do
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "Comment", line_idx, 0, 1)
vim.api.nvim_buf_add_highlight(state.queue_buf, ns_queue, "Normal", line_idx, 2, -1)
line_idx = line_idx + 1
end
vim.bo[state.queue_buf].modifiable = false
end)
vim.bo[state.queue_buf].modifiable = false
end)
end
--- Open the logs panel
function M.open()
if state.is_open then
return
end
if state.is_open then
return
end
-- Clear previous logs
logs.clear()
-- Clear previous logs
logs.clear()
-- Create logs buffer
state.buf = vim.api.nvim_create_buf(false, true)
vim.bo[state.buf].buftype = "nofile"
vim.bo[state.buf].bufhidden = "hide"
vim.bo[state.buf].swapfile = false
-- Create logs buffer
state.buf = vim.api.nvim_create_buf(false, true)
vim.bo[state.buf].buftype = "nofile"
vim.bo[state.buf].bufhidden = "hide"
vim.bo[state.buf].swapfile = false
-- Create window on the right
vim.cmd("botright vsplit")
state.win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(state.win, state.buf)
vim.api.nvim_win_set_width(state.win, LOGS_WIDTH)
-- Create window on the right
vim.cmd("botright vsplit")
state.win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(state.win, state.buf)
vim.api.nvim_win_set_width(state.win, LOGS_WIDTH)
-- Window options for logs
vim.wo[state.win].number = false
vim.wo[state.win].relativenumber = false
vim.wo[state.win].signcolumn = "no"
vim.wo[state.win].wrap = true
vim.wo[state.win].linebreak = true
vim.wo[state.win].winfixwidth = true
vim.wo[state.win].cursorline = false
-- Window options for logs
vim.wo[state.win].number = false
vim.wo[state.win].relativenumber = false
vim.wo[state.win].signcolumn = "no"
vim.wo[state.win].wrap = true
vim.wo[state.win].linebreak = true
vim.wo[state.win].winfixwidth = true
vim.wo[state.win].cursorline = false
-- Set initial content for logs
vim.bo[state.buf].modifiable = true
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, {
"Generation Logs",
string.rep("", LOGS_WIDTH - 2),
"",
})
vim.bo[state.buf].modifiable = false
-- Set initial content for logs
vim.bo[state.buf].modifiable = true
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, {
"Generation Logs",
string.rep("", LOGS_WIDTH - 2),
"",
})
vim.bo[state.buf].modifiable = false
-- Create queue buffer
state.queue_buf = vim.api.nvim_create_buf(false, true)
vim.bo[state.queue_buf].buftype = "nofile"
vim.bo[state.queue_buf].bufhidden = "hide"
vim.bo[state.queue_buf].swapfile = false
-- Create queue buffer
state.queue_buf = vim.api.nvim_create_buf(false, true)
vim.bo[state.queue_buf].buftype = "nofile"
vim.bo[state.queue_buf].bufhidden = "hide"
vim.bo[state.queue_buf].swapfile = false
-- Create queue window as horizontal split at bottom of logs window
vim.cmd("belowright split")
state.queue_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(state.queue_win, state.queue_buf)
vim.api.nvim_win_set_height(state.queue_win, QUEUE_HEIGHT)
-- Create queue window as horizontal split at bottom of logs window
vim.cmd("belowright split")
state.queue_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(state.queue_win, state.queue_buf)
vim.api.nvim_win_set_height(state.queue_win, QUEUE_HEIGHT)
-- Window options for queue
vim.wo[state.queue_win].number = false
vim.wo[state.queue_win].relativenumber = false
vim.wo[state.queue_win].signcolumn = "no"
vim.wo[state.queue_win].wrap = true
vim.wo[state.queue_win].linebreak = true
vim.wo[state.queue_win].winfixheight = true
vim.wo[state.queue_win].cursorline = false
-- Window options for queue
vim.wo[state.queue_win].number = false
vim.wo[state.queue_win].relativenumber = false
vim.wo[state.queue_win].signcolumn = "no"
vim.wo[state.queue_win].wrap = true
vim.wo[state.queue_win].linebreak = true
vim.wo[state.queue_win].winfixheight = true
vim.wo[state.queue_win].cursorline = false
-- Setup keymaps for logs buffer
local opts = { buffer = state.buf, noremap = true, silent = true }
vim.keymap.set("n", "q", M.close, opts)
vim.keymap.set("n", "<Esc>", M.close, opts)
-- Setup keymaps for logs buffer
local opts = { buffer = state.buf, noremap = true, silent = true }
vim.keymap.set("n", "q", M.close, opts)
vim.keymap.set("n", "<Esc>", M.close, opts)
-- Setup keymaps for queue buffer
local queue_opts = { buffer = state.queue_buf, noremap = true, silent = true }
vim.keymap.set("n", "q", M.close, queue_opts)
vim.keymap.set("n", "<Esc>", M.close, queue_opts)
-- Setup keymaps for queue buffer
local queue_opts = { buffer = state.queue_buf, noremap = true, silent = true }
vim.keymap.set("n", "q", M.close, queue_opts)
vim.keymap.set("n", "<Esc>", M.close, queue_opts)
-- Register log listener
state.listener_id = logs.add_listener(function(entry)
add_log_entry(entry)
if entry.level == "response" then
vim.schedule(update_title)
end
end)
-- Register log listener
state.listener_id = logs.add_listener(function(entry)
add_log_entry(entry)
if entry.level == "response" then
vim.schedule(update_title)
end
end)
-- Register queue listener
state.queue_listener_id = queue.add_listener(function()
update_queue_display()
end)
-- Register queue listener
state.queue_listener_id = queue.add_listener(function()
update_queue_display()
end)
-- Initial queue display
update_queue_display()
-- Initial queue display
update_queue_display()
state.is_open = true
state.is_open = true
-- Return focus to previous window
vim.cmd("wincmd p")
-- Return focus to previous window
vim.cmd("wincmd p")
logs.info("Logs panel opened")
logs.info("Logs panel opened")
end
--- Close the logs panel
---@param force? boolean Force close even if not marked as open
function M.close(force)
if not state.is_open and not force then
return
end
if not state.is_open and not force then
return
end
-- Remove log listener
if state.listener_id then
pcall(logs.remove_listener, state.listener_id)
state.listener_id = nil
end
-- Remove log listener
if state.listener_id then
pcall(logs.remove_listener, state.listener_id)
state.listener_id = nil
end
-- Remove queue listener
if state.queue_listener_id then
pcall(queue.remove_listener, state.queue_listener_id)
state.queue_listener_id = nil
end
-- Remove queue listener
if state.queue_listener_id then
pcall(queue.remove_listener, state.queue_listener_id)
state.queue_listener_id = nil
end
-- Close queue window first
if state.queue_win then
pcall(vim.api.nvim_win_close, state.queue_win, true)
state.queue_win = nil
end
-- Close queue window first
if state.queue_win then
pcall(vim.api.nvim_win_close, state.queue_win, true)
state.queue_win = nil
end
-- Close logs window
if state.win then
pcall(vim.api.nvim_win_close, state.win, true)
state.win = nil
end
-- Close logs window
if state.win then
pcall(vim.api.nvim_win_close, state.win, true)
state.win = nil
end
-- Delete queue buffer
if state.queue_buf then
pcall(vim.api.nvim_buf_delete, state.queue_buf, { force = true })
state.queue_buf = nil
end
-- Delete queue buffer
if state.queue_buf then
pcall(vim.api.nvim_buf_delete, state.queue_buf, { force = true })
state.queue_buf = nil
end
-- Delete logs buffer
if state.buf then
pcall(vim.api.nvim_buf_delete, state.buf, { force = true })
state.buf = nil
end
-- Delete logs buffer
if state.buf then
pcall(vim.api.nvim_buf_delete, state.buf, { force = true })
state.buf = nil
end
state.is_open = false
state.is_open = false
end
--- Toggle the logs panel
function M.toggle()
if state.is_open then
M.close()
else
M.open()
end
if state.is_open then
M.close()
else
M.open()
end
end
--- Check if panel is open
---@return boolean
function M.is_open()
return state.is_open
return state.is_open
end
--- Ensure panel is open (call before starting generation)
function M.ensure_open()
if not state.is_open then
M.open()
end
if not state.is_open then
M.open()
end
end
--- Setup autocmds for the logs panel
function M.setup()
local group = vim.api.nvim_create_augroup("CodetypeLogsPanel", { clear = true })
local group = vim.api.nvim_create_augroup("CodetypeLogsPanel", { clear = true })
-- Close logs panel when exiting Neovim
vim.api.nvim_create_autocmd("VimLeavePre", {
group = group,
callback = function()
-- Force close to ensure cleanup even in edge cases
M.close(true)
end,
desc = "Close logs panel before exiting Neovim",
})
-- Close logs panel when exiting Neovim
vim.api.nvim_create_autocmd("VimLeavePre", {
group = group,
callback = function()
-- Force close to ensure cleanup even in edge cases
M.close(true)
end,
desc = "Close logs panel before exiting Neovim",
})
-- Also clean up when QuitPre fires (handles :qa, :wqa, etc.)
vim.api.nvim_create_autocmd("QuitPre", {
group = group,
callback = function()
-- Check if this is the last window (about to quit Neovim)
local wins = vim.api.nvim_list_wins()
local real_wins = 0
for _, win in ipairs(wins) do
local buf = vim.api.nvim_win_get_buf(win)
local buftype = vim.bo[buf].buftype
-- Count non-special windows
if buftype == "" or buftype == "help" then
real_wins = real_wins + 1
end
end
-- If only logs/queue windows remain, close them
if real_wins <= 1 then
M.close(true)
end
end,
desc = "Close logs panel on quit",
})
-- Also clean up when QuitPre fires (handles :qa, :wqa, etc.)
vim.api.nvim_create_autocmd("QuitPre", {
group = group,
callback = function()
-- Check if this is the last window (about to quit Neovim)
local wins = vim.api.nvim_list_wins()
local real_wins = 0
for _, win in ipairs(wins) do
local buf = vim.api.nvim_win_get_buf(win)
local buftype = vim.bo[buf].buftype
-- Count non-special windows
if buftype == "" or buftype == "help" then
real_wins = real_wins + 1
end
end
-- If only logs/queue windows remain, close them
if real_wins <= 1 then
M.close(true)
end
end,
desc = "Close logs panel on quit",
})
end
return M

View File

@@ -30,7 +30,7 @@ local defaults = {
auto_index = true, -- Index files on save
index_on_open = false, -- Index project when opening
max_file_size = 100000, -- Skip files larger than 100KB
excluded_dirs = { "node_modules", "dist", "build", ".git", ".coder", "__pycache__", "vendor", "target" },
excluded_dirs = { "node_modules", "dist", "build", ".git", ".codetyper", "__pycache__", "vendor", "target" },
index_extensions = { "lua", "ts", "tsx", "js", "jsx", "py", "go", "rs", "rb", "java", "c", "cpp", "h", "hpp" },
memory = {
enabled = true, -- Enable memory persistence

View File

@@ -1,6 +1,6 @@
---@mod codetyper.preferences User preferences management
---@brief [[
--- Manages user preferences stored in .coder/preferences.json
--- Manages user preferences stored in .codetyper/preferences.json
--- Allows per-project configuration of plugin behavior.
---@brief ]]
@@ -9,13 +9,11 @@ local M = {}
local utils = require("codetyper.support.utils")
---@class CoderPreferences
---@field auto_process boolean Whether to auto-process /@ @/ tags (default: nil = ask)
---@field asked_auto_process boolean Whether we've asked the user about auto_process
--- Default preferences
local defaults = {
auto_process = nil, -- nil means "not yet decided"
asked_auto_process = false,
auto_process = nil, -- nil means "not yet decided"
asked_auto_process = false,
}
--- Cached preferences per project
@@ -25,190 +23,113 @@ local cache = {}
--- Get the preferences file path for current project
---@return string
local function get_preferences_path()
local cwd = vim.fn.getcwd()
return cwd .. "/.coder/preferences.json"
local cwd = vim.fn.getcwd()
return cwd .. "/.codetyper/preferences.json"
end
--- Ensure .coder directory exists
--- Ensure .codetyper directory exists
local function ensure_coder_dir()
local cwd = vim.fn.getcwd()
local coder_dir = cwd .. "/.coder"
if vim.fn.isdirectory(coder_dir) == 0 then
vim.fn.mkdir(coder_dir, "p")
end
local cwd = vim.fn.getcwd()
local coder_dir = cwd .. "/.codetyper"
if vim.fn.isdirectory(coder_dir) == 0 then
vim.fn.mkdir(coder_dir, "p")
end
end
--- Load preferences from file
---@return CoderPreferences
function M.load()
local cwd = vim.fn.getcwd()
local cwd = vim.fn.getcwd()
-- Check cache first
if cache[cwd] then
return cache[cwd]
end
-- Check cache first
if cache[cwd] then
return cache[cwd]
end
local path = get_preferences_path()
local prefs = vim.deepcopy(defaults)
local path = get_preferences_path()
local prefs = vim.deepcopy(defaults)
if utils.file_exists(path) then
local content = utils.read_file(path)
if content then
local ok, decoded = pcall(vim.json.decode, content)
if ok and decoded then
-- Merge with defaults
for k, v in pairs(decoded) do
prefs[k] = v
end
end
end
end
if utils.file_exists(path) then
local content = utils.read_file(path)
if content then
local ok, decoded = pcall(vim.json.decode, content)
if ok and decoded then
-- Merge with defaults
for k, v in pairs(decoded) do
prefs[k] = v
end
end
end
end
-- Cache it
cache[cwd] = prefs
return prefs
-- Cache it
cache[cwd] = prefs
return prefs
end
--- Save preferences to file
---@param prefs CoderPreferences
function M.save(prefs)
local cwd = vim.fn.getcwd()
ensure_coder_dir()
local cwd = vim.fn.getcwd()
ensure_coder_dir()
local path = get_preferences_path()
local ok, encoded = pcall(vim.json.encode, prefs)
if ok then
utils.write_file(path, encoded)
-- Update cache
cache[cwd] = prefs
end
local path = get_preferences_path()
local ok, encoded = pcall(vim.json.encode, prefs)
if ok then
utils.write_file(path, encoded)
-- Update cache
cache[cwd] = prefs
end
end
--- Get a specific preference
---@param key string
---@return any
function M.get(key)
local prefs = M.load()
return prefs[key]
local prefs = M.load()
return prefs[key]
end
--- Set a specific preference
---@param key string
---@param value any
function M.set(key, value)
local prefs = M.load()
prefs[key] = value
M.save(prefs)
local prefs = M.load()
prefs[key] = value
M.save(prefs)
end
--- Check if auto-process is enabled
---@return boolean|nil Returns true/false if set, nil if not yet decided
function M.is_auto_process_enabled()
return M.get("auto_process")
return M.get("auto_process")
end
--- Set auto-process preference
---@param enabled boolean
function M.set_auto_process(enabled)
M.set("auto_process", enabled)
M.set("asked_auto_process", true)
M.set("auto_process", enabled)
M.set("asked_auto_process", true)
end
--- Check if we've already asked the user about auto-process
---@return boolean
function M.has_asked_auto_process()
return M.get("asked_auto_process") == true
end
--- Ask user about auto-process preference (shows floating window)
---@param callback function(enabled: boolean) Called with user's choice
function M.ask_auto_process_preference(callback)
-- Check if already asked
if M.has_asked_auto_process() then
local enabled = M.is_auto_process_enabled()
if enabled ~= nil then
callback(enabled)
return
end
end
-- Create floating window to ask
local width = 60
local height = 7
local row = math.floor((vim.o.lines - height) / 2)
local col = math.floor((vim.o.columns - width) / 2)
local buf = vim.api.nvim_create_buf(false, true)
vim.bo[buf].buftype = "nofile"
vim.bo[buf].bufhidden = "wipe"
local win = vim.api.nvim_open_win(buf, true, {
relative = "editor",
row = row,
col = col,
width = width,
height = height,
style = "minimal",
border = "rounded",
title = " Codetyper Preferences ",
title_pos = "center",
})
local lines = {
"",
" How would you like to process /@ @/ prompt tags?",
"",
" [a] Automatic - Process when you close the tag",
" [m] Manual - Only process with :CoderProcess",
"",
" Press 'a' or 'm' to choose (Esc to cancel)",
}
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
vim.bo[buf].modifiable = false
-- Highlight
local ns = vim.api.nvim_create_namespace("codetyper_prefs")
vim.api.nvim_buf_add_highlight(buf, ns, "Title", 1, 0, -1)
vim.api.nvim_buf_add_highlight(buf, ns, "String", 3, 2, 5)
vim.api.nvim_buf_add_highlight(buf, ns, "String", 4, 2, 5)
local function close_and_callback(enabled)
if vim.api.nvim_win_is_valid(win) then
vim.api.nvim_win_close(win, true)
end
if enabled ~= nil then
M.set_auto_process(enabled)
local mode = enabled and "automatic" or "manual"
vim.notify("Codetyper: Set to " .. mode .. " mode (saved to .coder/preferences.json)", vim.log.levels.INFO)
end
if callback then
callback(enabled)
end
end
-- Keymaps
local opts = { buffer = buf, noremap = true, silent = true }
vim.keymap.set("n", "a", function() close_and_callback(true) end, opts)
vim.keymap.set("n", "A", function() close_and_callback(true) end, opts)
vim.keymap.set("n", "m", function() close_and_callback(false) end, opts)
vim.keymap.set("n", "M", function() close_and_callback(false) end, opts)
vim.keymap.set("n", "<Esc>", function() close_and_callback(nil) end, opts)
vim.keymap.set("n", "q", function() close_and_callback(nil) end, opts)
return M.get("asked_auto_process") == true
end
--- Clear cached preferences (useful when changing projects)
function M.clear_cache()
cache = {}
cache = {}
end
--- Toggle auto-process mode
function M.toggle_auto_process()
local current = M.is_auto_process_enabled()
local new_value = not current
M.set_auto_process(new_value)
local mode = new_value and "automatic" or "manual"
vim.notify("Codetyper: Switched to " .. mode .. " mode", vim.log.levels.INFO)
local current = M.is_auto_process_enabled()
local new_value = not current
M.set_auto_process(new_value)
local mode = new_value and "automatic" or "manual"
vim.notify("Codetyper: Switched to " .. mode .. " mode", vim.log.levels.INFO)
end
return M

View File

@@ -15,7 +15,7 @@ local COST_HISTORY_FILE = "cost_history.json"
---@return string File path
local function get_history_path()
local root = utils.get_project_root()
return root .. "/.coder/" .. COST_HISTORY_FILE
return root .. "/.codetyper/" .. COST_HISTORY_FILE
end
--- Default model for savings comparison (what you'd pay if not using Ollama)

View File

@@ -213,9 +213,9 @@ function M.create_from_event(event, generated_code, confidence, strategy)
end
end
-- Detect if this is an inline prompt (source == target, not a .coder. file)
-- Detect if this is an inline prompt (source == target, not a .codetyper/ file)
local is_inline = (source_bufnr == target_bufnr) or
(event.target_path and not event.target_path:match("%.coder%."))
(event.target_path and not event.target_path:match("%.codetyper%."))
-- Take snapshot of the scope range in target buffer (for staleness detection)
local snapshot_range = event.scope_range or event.range
@@ -452,89 +452,6 @@ function M.mark_rejected(id, reason)
return false
end
--- Remove /@ @/ prompt tags from buffer
---@param bufnr number Buffer number
---@return number Number of tag regions removed
local function remove_prompt_tags(bufnr)
if not vim.api.nvim_buf_is_valid(bufnr) then
return 0
end
local removed = 0
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
-- Find and remove all /@ ... @/ regions (can be multiline)
local i = 1
while i <= #lines do
local line = lines[i]
local open_start = line:find("/@")
if open_start then
-- Found an opening tag, look for closing tag
local close_end = nil
local close_line = i
-- Check if closing tag is on same line
local after_open = line:sub(open_start + 2)
local same_line_close = after_open:find("@/")
if same_line_close then
-- Single line tag - remove just this portion
local before = line:sub(1, open_start - 1)
local after = line:sub(open_start + 2 + same_line_close + 1)
lines[i] = before .. after
-- If line is now empty or just whitespace, remove it
if lines[i]:match("^%s*$") then
table.remove(lines, i)
else
i = i + 1
end
removed = removed + 1
else
-- Multi-line tag - find the closing line
for j = i, #lines do
if lines[j]:find("@/") then
close_line = j
close_end = lines[j]:find("@/")
break
end
end
if close_end then
-- Remove lines from i to close_line
-- Keep content before /@ on first line and after @/ on last line
local before = lines[i]:sub(1, open_start - 1)
local after = lines[close_line]:sub(close_end + 2)
-- Remove the lines containing the tag
for _ = i, close_line do
table.remove(lines, i)
end
-- If there's content to keep, insert it back
local remaining = (before .. after):match("^%s*(.-)%s*$")
if remaining and remaining ~= "" then
table.insert(lines, i, remaining)
i = i + 1
end
removed = removed + 1
else
-- No closing tag found, skip this line
i = i + 1
end
end
else
i = i + 1
end
end
if removed > 0 then
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
end
return removed
end
--- Check if it's safe to modify the buffer (not in insert or visual mode)
---@return boolean
local function is_safe_to_modify()
@@ -625,46 +542,9 @@ function M.apply(patch)
local is_inline_prompt = patch.is_inline_prompt or (source_bufnr == target_bufnr)
local tags_removed = 0
-- For CODER FILES (source != target): Remove tags from source, inject into target
-- For INLINE PROMPTS (source == target): Include tag range in injection, no separate removal
if not is_inline_prompt and source_bufnr and vim.api.nvim_buf_is_valid(source_bufnr) then
tags_removed = remove_prompt_tags(source_bufnr)
pcall(function()
if tags_removed > 0 then
local logs = require("codetyper.adapters.nvim.ui.logs")
local source_name = vim.api.nvim_buf_get_name(source_bufnr)
logs.add({
type = "info",
message = string.format("Removed %d prompt tag(s) from %s",
tags_removed,
vim.fn.fnamemodify(source_name, ":t")),
})
end
end)
end
-- Get filetype for smart injection
local filetype = vim.fn.fnamemodify(patch.target_path or "", ":e")
-- SEARCH/REPLACE MODE: Use fuzzy matching to find and replace text
if patch.use_search_replace and patch.search_replace_blocks and #patch.search_replace_blocks > 0 then
local search_replace = get_search_replace_module()
-- Remove the /@ @/ tags first (they shouldn't be in the file anymore)
if is_inline_prompt and source_bufnr and vim.api.nvim_buf_is_valid(source_bufnr) then
tags_removed = remove_prompt_tags(source_bufnr)
if tags_removed > 0 then
pcall(function()
local logs = require("codetyper.adapters.nvim.ui.logs")
logs.add({
type = "info",
message = string.format("Removed %d prompt tag(s)", tags_removed),
})
end)
end
end
-- Apply SEARCH/REPLACE blocks
local success, err = search_replace.apply_to_buffer(target_bufnr, patch.search_replace_blocks)
@@ -1104,11 +984,7 @@ function M.apply_with_conflict(patch)
local source_bufnr = patch.source_bufnr
local is_inline_prompt = patch.is_inline_prompt or (source_bufnr == target_bufnr)
-- Remove tags from coder files
if not is_inline_prompt and source_bufnr and vim.api.nvim_buf_is_valid(source_bufnr) then
remove_prompt_tags(source_bufnr)
end
-- For SEARCH/REPLACE blocks, convert each block to a conflict
if patch.use_search_replace and patch.search_replace_blocks and #patch.search_replace_blocks > 0 then
local search_replace = get_search_replace_module()
@@ -1147,11 +1023,6 @@ function M.apply_with_conflict(patch)
end
if applied_count > 0 then
-- Remove tags for inline prompts after inserting conflicts
if is_inline_prompt and source_bufnr and vim.api.nvim_buf_is_valid(source_bufnr) then
remove_prompt_tags(source_bufnr)
end
-- Process conflicts (highlight, keymaps) and show menu
conflict.process_and_show_menu(target_bufnr)
@@ -1178,11 +1049,6 @@ function M.apply_with_conflict(patch)
local end_line = patch.injection_range.end_line
local new_lines = vim.split(patch.generated_code, "\n", { plain = true })
-- Remove tags for inline prompts
if is_inline_prompt and source_bufnr and vim.api.nvim_buf_is_valid(source_bufnr) then
remove_prompt_tags(source_bufnr)
end
-- Insert conflict markers
conflict.insert_conflict(target_bufnr, start_line, end_line, new_lines, "AI SUGGESTION")

View File

@@ -25,7 +25,7 @@ local DEBOUNCE_MS = 500
---@return string Brain directory path
function M.get_brain_dir(root)
root = root or utils.get_project_root()
return root .. "/.coder/brain"
return root .. "/.codetyper/brain"
end
--- Ensure brain directory structure exists

View File

@@ -10,7 +10,7 @@ local utils = require("codetyper.support.utils")
---@return string|nil
local function get_resume_dir()
local root = utils.get_project_root() or vim.fn.getcwd()
return root .. "/.coder/tmp"
return root .. "/.codetyper/tmp"
end
--- Get the resume context file path

View File

@@ -468,7 +468,7 @@ local function dispatch_next()
local thinking = require("codetyper.adapters.nvim.ui.thinking")
thinking.ensure_shown()
local is_inline = event.target_path and not event.target_path:match("%.coder%.") and (event.bufnr == vim.fn.bufnr(event.target_path))
local is_inline = event.target_path and not event.target_path:match("%.codetyper%.") and (event.bufnr == vim.fn.bufnr(event.target_path))
local thinking_placeholder = require("codetyper.core.thinking_placeholder")
if is_inline then
-- 99-style: virtual text "⠋ Thinking..." at selection (no buffer change, SEARCH/REPLACE safe)

View File

@@ -258,7 +258,7 @@ local function get_coder_companion_path(target_path)
end
-- Skip if target is already a coder file
if target_path:match("%.coder%.") then
if target_path:match("%.codetyper%.") then
return nil
end
@@ -266,7 +266,7 @@ local function get_coder_companion_path(target_path)
local name = vim.fn.fnamemodify(target_path, ":t:r") -- filename without extension
local ext = vim.fn.fnamemodify(target_path, ":e")
local coder_path = dir .. "/" .. name .. ".coder." .. ext
local coder_path = dir .. "/" .. name .. ".codetyper/" .. ext
if vim.fn.filereadable(coder_path) == 1 then
return coder_path
end
@@ -382,13 +382,13 @@ end
---@return boolean
local function is_inline_prompt(event)
-- Inline prompts have a range with start_line/end_line from tag detection
-- and the source file is the same as target (not a .coder. file)
-- and the source file is the same as target (not a .codetyper/ file)
if not event.range or not event.range.start_line then
return false
end
-- Check if source path (if any) equals target, or if target has no .coder. in it
-- Check if source path (if any) equals target, or if target has no .codetyper/ in it
local target = event.target_path or ""
if target:match("%.coder%.") then
if target:match("%.codetyper%.") then
return false
end
return true

View File

@@ -1,7 +1,5 @@
---@mod codetyper.completion Insert mode completion for file references
---
--- Provides completion for @filename inside /@ @/ tags.
local M = {}
local parser = require("codetyper.parser")
@@ -11,182 +9,183 @@ local utils = require("codetyper.support.utils")
---@param prefix string Prefix to filter files
---@return table[] List of completion items
local function get_file_completions(prefix)
local cwd = vim.fn.getcwd()
local current_file = vim.fn.expand("%:p")
local current_dir = vim.fn.fnamemodify(current_file, ":h")
local files = {}
local cwd = vim.fn.getcwd()
local current_file = vim.fn.expand("%:p")
local current_dir = vim.fn.fnamemodify(current_file, ":h")
local files = {}
-- Use vim.fn.glob to find files matching the prefix
local pattern = prefix .. "*"
-- Use vim.fn.glob to find files matching the prefix
local pattern = prefix .. "*"
-- Determine base directory - use current file's directory if outside cwd
local base_dir = cwd
if current_dir ~= "" and not current_dir:find(cwd, 1, true) then
-- File is outside project, use its directory as base
base_dir = current_dir
end
-- Determine base directory - use current file's directory if outside cwd
local base_dir = cwd
if current_dir ~= "" and not current_dir:find(cwd, 1, true) then
-- File is outside project, use its directory as base
base_dir = current_dir
end
-- Search in base directory
local matches = vim.fn.glob(base_dir .. "/" .. pattern, false, true)
-- Search in base directory
local matches = vim.fn.glob(base_dir .. "/" .. pattern, false, true)
-- Search with ** for all subdirectories
local deep_matches = vim.fn.glob(base_dir .. "/**/" .. pattern, false, true)
for _, m in ipairs(deep_matches) do
table.insert(matches, m)
end
-- Search with ** for all subdirectories
local deep_matches = vim.fn.glob(base_dir .. "/**/" .. pattern, false, true)
for _, m in ipairs(deep_matches) do
table.insert(matches, m)
end
-- Also search in cwd if different from base_dir
if base_dir ~= cwd then
local cwd_matches = vim.fn.glob(cwd .. "/" .. pattern, false, true)
for _, m in ipairs(cwd_matches) do
table.insert(matches, m)
end
local cwd_deep = vim.fn.glob(cwd .. "/**/" .. pattern, false, true)
for _, m in ipairs(cwd_deep) do
table.insert(matches, m)
end
end
-- Also search in cwd if different from base_dir
if base_dir ~= cwd then
local cwd_matches = vim.fn.glob(cwd .. "/" .. pattern, false, true)
for _, m in ipairs(cwd_matches) do
table.insert(matches, m)
end
local cwd_deep = vim.fn.glob(cwd .. "/**/" .. pattern, false, true)
for _, m in ipairs(cwd_deep) do
table.insert(matches, m)
end
end
-- Also search specific directories if prefix doesn't have path
if not prefix:find("/") then
local search_dirs = { "src", "lib", "lua", "app", "components", "utils", "tests" }
for _, dir in ipairs(search_dirs) do
local dir_path = base_dir .. "/" .. dir
if vim.fn.isdirectory(dir_path) == 1 then
local dir_matches = vim.fn.glob(dir_path .. "/**/" .. pattern, false, true)
for _, m in ipairs(dir_matches) do
table.insert(matches, m)
end
end
end
end
-- Also search specific directories if prefix doesn't have path
if not prefix:find("/") then
local search_dirs = { "src", "lib", "lua", "app", "components", "utils", "tests" }
for _, dir in ipairs(search_dirs) do
local dir_path = base_dir .. "/" .. dir
if vim.fn.isdirectory(dir_path) == 1 then
local dir_matches = vim.fn.glob(dir_path .. "/**/" .. pattern, false, true)
for _, m in ipairs(dir_matches) do
table.insert(matches, m)
end
end
end
end
-- Convert to relative paths and deduplicate
local seen = {}
for _, match in ipairs(matches) do
-- Convert to relative path based on which base it came from
local rel_path
if match:find(base_dir, 1, true) == 1 then
rel_path = match:sub(#base_dir + 2)
elseif match:find(cwd, 1, true) == 1 then
rel_path = match:sub(#cwd + 2)
else
rel_path = vim.fn.fnamemodify(match, ":t") -- Just filename if can't make relative
end
-- Convert to relative paths and deduplicate
local seen = {}
for _, match in ipairs(matches) do
-- Convert to relative path based on which base it came from
local rel_path
if match:find(base_dir, 1, true) == 1 then
rel_path = match:sub(#base_dir + 2)
elseif match:find(cwd, 1, true) == 1 then
rel_path = match:sub(#cwd + 2)
else
rel_path = vim.fn.fnamemodify(match, ":t") -- Just filename if can't make relative
end
-- Skip directories, coder files, and hidden/generated files
if vim.fn.isdirectory(match) == 0
and not utils.is_coder_file(match)
and not rel_path:match("^%.")
and not rel_path:match("node_modules")
and not rel_path:match("%.git/")
and not rel_path:match("dist/")
and not rel_path:match("build/")
and not seen[rel_path]
then
seen[rel_path] = true
table.insert(files, {
word = rel_path,
abbr = rel_path,
kind = "File",
menu = "[ref]",
})
end
end
-- Skip directories, coder files, and hidden/generated files
if
vim.fn.isdirectory(match) == 0
and not utils.is_coder_file(match)
and not rel_path:match("^%.")
and not rel_path:match("node_modules")
and not rel_path:match("%.git/")
and not rel_path:match("dist/")
and not rel_path:match("build/")
and not seen[rel_path]
then
seen[rel_path] = true
table.insert(files, {
word = rel_path,
abbr = rel_path,
kind = "File",
menu = "[ref]",
})
end
end
-- Sort by length (shorter paths first)
table.sort(files, function(a, b)
return #a.word < #b.word
end)
-- Sort by length (shorter paths first)
table.sort(files, function(a, b)
return #a.word < #b.word
end)
-- Limit results
local result = {}
for i = 1, math.min(#files, 15) do
result[i] = files[i]
end
-- Limit results
local result = {}
for i = 1, math.min(#files, 15) do
result[i] = files[i]
end
return result
return result
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()
if not is_inside then
return false
end
-- Check if we're in an open prompt tag
local is_inside = parser.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()
if prefix == nil then
return false
end
-- Get the prefix being typed
local prefix = parser.get_file_ref_prefix()
if prefix == nil then
return false
end
-- Get completions
local items = get_file_completions(prefix)
-- Get completions
local items = get_file_completions(prefix)
if #items == 0 then
-- Try with empty prefix to show all files
items = get_file_completions("")
end
if #items == 0 then
-- Try with empty prefix to show all files
items = get_file_completions("")
end
if #items > 0 then
-- Calculate start column (position right after @)
local cursor = vim.api.nvim_win_get_cursor(0)
local col = cursor[2] - #prefix + 1 -- 1-indexed for complete()
if #items > 0 then
-- Calculate start column (position right after @)
local cursor = vim.api.nvim_win_get_cursor(0)
local col = cursor[2] - #prefix + 1 -- 1-indexed for complete()
-- Show completion popup
vim.fn.complete(col, items)
return true
end
-- Show completion popup
vim.fn.complete(col, items)
return true
end
return false
return false
end
--- Setup completion for file references (works on ALL files)
function M.setup()
local group = vim.api.nvim_create_augroup("CoderCompletion", { clear = true })
local group = vim.api.nvim_create_augroup("CoderCompletion", { clear = true })
-- Trigger completion on @ in insert mode (works on ALL files)
vim.api.nvim_create_autocmd("InsertCharPre", {
group = group,
pattern = "*",
callback = function()
-- Skip special buffers
if vim.bo.buftype ~= "" then
return
end
-- Trigger completion on @ in insert mode (works on ALL files)
vim.api.nvim_create_autocmd("InsertCharPre", {
group = group,
pattern = "*",
callback = function()
-- Skip special buffers
if vim.bo.buftype ~= "" then
return
end
if vim.v.char == "@" then
-- 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()
if not is_inside then
return
end
if vim.v.char == "@" then
-- 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()
if not is_inside then
return
end
-- Check we're not typing @/ (closing tag)
local cursor = vim.api.nvim_win_get_cursor(0)
local line = vim.api.nvim_get_current_line()
local next_char = line:sub(cursor[2] + 2, cursor[2] + 2)
-- Check we're not typing @/ (closing tag)
local cursor = vim.api.nvim_win_get_cursor(0)
local line = vim.api.nvim_get_current_line()
local next_char = line:sub(cursor[2] + 2, cursor[2] + 2)
if next_char == "/" then
return
end
if next_char == "/" then
return
end
-- Show file completion
M.show_file_completion()
end)
end
end,
desc = "Trigger file completion on @ inside prompt tags",
})
-- Show file completion
M.show_file_completion()
end)
end
end,
desc = "Trigger file completion on @ inside prompt tags",
})
-- Also allow manual trigger with <C-x><C-f> style keybinding in insert mode
vim.keymap.set("i", "<C-x>@", function()
M.show_file_completion()
end, { silent = true, desc = "Coder: Complete file reference" })
-- Also allow manual trigger with <C-x><C-f> style keybinding in insert mode
vim.keymap.set("i", "<C-x>@", function()
M.show_file_completion()
end, { silent = true, desc = "Coder: Complete file reference" })
end
return M

View File

@@ -1,7 +1,7 @@
---@mod codetyper.indexer Project indexer for Codetyper.nvim
---@brief [[
--- Indexes project structure, dependencies, and code symbols.
--- Stores knowledge in .coder/ directory for enriching LLM context.
--- Stores knowledge in .codetyper/ directory for enriching LLM context.
---@brief ]]
local M = {}
@@ -24,7 +24,7 @@ local default_config = {
auto_index = true,
index_on_open = false,
max_file_size = 100000,
excluded_dirs = { "node_modules", "dist", "build", ".git", ".coder", "__pycache__", "vendor", "target" },
excluded_dirs = { "node_modules", "dist", "build", ".git", ".codetyper", "__pycache__", "vendor", "target" },
index_extensions = { "lua", "ts", "tsx", "js", "jsx", "py", "go", "rs", "rb", "java", "c", "cpp", "h", "hpp" },
memory = {
enabled = true,
@@ -94,7 +94,7 @@ local function get_index_path()
if not root then
return nil
end
return root .. "/.coder/" .. INDEX_FILE
return root .. "/.codetyper/" .. INDEX_FILE
end
--- Create empty index structure
@@ -168,8 +168,8 @@ function M.save_index(index)
return false
end
-- Ensure .coder directory exists
local coder_dir = root .. "/.coder"
-- Ensure .codetyper directory exists
local coder_dir = root .. "/.codetyper"
utils.ensure_dir(coder_dir)
local path = get_index_path()

View File

@@ -1,6 +1,6 @@
---@mod codetyper.indexer.memory Memory persistence manager
---@brief [[
--- Stores and retrieves learned patterns and memories in .coder/memories/.
--- Stores and retrieves learned patterns and memories in .codetyper/memories/.
--- Supports session history for learning from interactions.
---@brief ]]
@@ -42,7 +42,7 @@ local function get_memories_dir()
if not root then
return nil
end
return root .. "/.coder/" .. MEMORIES_DIR
return root .. "/.codetyper/" .. MEMORIES_DIR
end
--- Get the sessions directory
@@ -52,7 +52,7 @@ local function get_sessions_dir()
if not root then
return nil
end
return root .. "/.coder/" .. SESSIONS_DIR
return root .. "/.codetyper/" .. SESSIONS_DIR
end
--- Ensure memories directory exists

View File

@@ -44,7 +44,7 @@ local DEFAULT_IGNORES = {
"^node_modules$",
"^__pycache__$",
"^%.git$",
"^%.coder$",
"^%.codetyper$",
"^dist$",
"^build$",
"^target$",

View File

@@ -2,7 +2,7 @@
---@brief [[
--- Codetyper.nvim is a Neovim plugin that acts as your coding partner.
--- It uses LLM APIs (OpenAI, Gemini, Copilot, Ollama) to help you
--- write code faster using special `.coder.*` files and inline prompt tags.
--- write code faster using special `.codetyper/*` files and inline prompt tags.
--- Features an event-driven scheduler with confidence scoring and
--- completion-aware injection timing.
---@brief ]]
@@ -44,7 +44,7 @@ function M.setup(opts)
-- Ensure .gitignore has coder files excluded
gitignore.ensure_ignored()
-- Initialize tree logging (creates .coder folder and initial tree.log)
-- Initialize tree logging (creates .codetyper folder and initial tree.log)
tree.setup()
-- Initialize project indexer if enabled

View File

@@ -5,31 +5,15 @@ local M = {}
local utils = require("codetyper.support.utils")
local logger = require("codetyper.support.logger")
--- Get config with safe fallback
---@return table config
local function get_config_safe()
logger.func_entry("parser", "get_config_safe", {})
local ok, codetyper = pcall(require, "codetyper")
if ok and codetyper.get_config then
local config = codetyper.get_config()
if config and config.patterns then
logger.debug("parser", "get_config_safe: loaded config from codetyper")
logger.func_exit("parser", "get_config_safe", "success")
return config
end
end
logger.debug("parser", "get_config_safe: using fallback defaults")
logger.func_exit("parser", "get_config_safe", "fallback")
-- Fallback defaults
return {
patterns = {
open_tag = "/@",
close_tag = "@/",
},
}
-- Get current codetyper configuration at call time
local function get_config()
local ok, codetyper = pcall(require, "codetyper")
if ok and codetyper.get_config then
return codetyper.get_config() or {}
end
-- Fall back to defaults if codetyper isn't available
local defaults = require("codetyper.config.defaults")
return defaults.get_defaults()
end
--- Find all prompts in buffer content
@@ -41,9 +25,9 @@ function M.find_prompts(content, open_tag, close_tag)
logger.func_entry("parser", "find_prompts", {
content_length = #content,
open_tag = open_tag,
close_tag = close_tag
close_tag = close_tag,
})
local prompts = {}
local escaped_open = utils.escape_pattern(open_tag)
local escaped_close = utils.escape_pattern(close_tag)
@@ -94,7 +78,13 @@ function M.find_prompts(content, open_tag, close_tag)
current_prompt.end_line = line_num
current_prompt.end_col = end_col + #close_tag - 1
table.insert(prompts, current_prompt)
logger.debug("parser", "find_prompts: multi-line prompt completed at line " .. line_num .. ", total lines: " .. #prompt_content)
logger.debug(
"parser",
"find_prompts: multi-line prompt completed at line "
.. line_num
.. ", total lines: "
.. #prompt_content
)
in_prompt = false
current_prompt = nil
prompt_content = {}
@@ -106,7 +96,7 @@ function M.find_prompts(content, open_tag, close_tag)
logger.debug("parser", "find_prompts: found " .. #prompts .. " prompts total")
logger.func_exit("parser", "find_prompts", "found " .. #prompts .. " prompts")
return prompts
end
@@ -115,16 +105,18 @@ end
---@return CoderPrompt[] List of found prompts
function M.find_prompts_in_buffer(bufnr)
logger.func_entry("parser", "find_prompts_in_buffer", { bufnr = bufnr })
local config = get_config_safe()
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local content = table.concat(lines, "\n")
logger.debug("parser", "find_prompts_in_buffer: bufnr=" .. bufnr .. ", lines=" .. #lines .. ", content_length=" .. #content)
logger.debug(
"parser",
"find_prompts_in_buffer: bufnr=" .. bufnr .. ", lines=" .. #lines .. ", content_length=" .. #content
)
local cfg = get_config()
local result = M.find_prompts(content, cfg.patterns.open_tag, cfg.patterns.close_tag)
local result = M.find_prompts(content, config.patterns.open_tag, config.patterns.close_tag)
logger.func_exit("parser", "find_prompts_in_buffer", "found " .. #result .. " prompts")
return result
end
@@ -141,7 +133,7 @@ function M.get_prompt_at_cursor(bufnr)
logger.func_entry("parser", "get_prompt_at_cursor", {
bufnr = bufnr,
line = line,
col = col
col = col,
})
local prompts = M.find_prompts_in_buffer(bufnr)
@@ -149,15 +141,30 @@ function M.get_prompt_at_cursor(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 .. ")")
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)
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)
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")
@@ -177,9 +184,9 @@ end
---@return CoderPrompt|nil Last prompt or nil
function M.get_last_prompt(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
logger.func_entry("parser", "get_last_prompt", { bufnr = bufnr })
local prompts = M.find_prompts_in_buffer(bufnr)
if #prompts > 0 then
@@ -199,7 +206,7 @@ end
---@return "refactor" | "add" | "document" | "explain" | "generic" Prompt type
function M.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
@@ -230,15 +237,15 @@ end
---@return string Cleaned content
function M.clean_prompt(content)
logger.func_entry("parser", "clean_prompt", { content_length = #content })
-- Trim leading/trailing whitespace
content = content:match("^%s*(.-)%s*$")
-- Normalize multiple newlines
content = content:gsub("\n\n\n+", "\n\n")
logger.debug("parser", "clean_prompt: cleaned from " .. #content .. " chars")
logger.func_exit("parser", "clean_prompt", "length=" .. #content)
return content
end
@@ -248,12 +255,12 @@ end
---@return boolean
function M.has_closing_tag(line, close_tag)
logger.func_entry("parser", "has_closing_tag", { line_preview = line:sub(1, 30), close_tag = close_tag })
local result = line:find(utils.escape_pattern(close_tag)) ~= nil
logger.debug("parser", "has_closing_tag: result=" .. tostring(result))
logger.func_exit("parser", "has_closing_tag", result)
return result
end
@@ -262,25 +269,32 @@ end
---@return boolean
function M.has_unclosed_prompts(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
logger.func_entry("parser", "has_unclosed_prompts", { bufnr = bufnr })
local config = get_config_safe()
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local content = table.concat(lines, "\n")
local escaped_open = utils.escape_pattern(config.patterns.open_tag)
local escaped_close = utils.escape_pattern(config.patterns.close_tag)
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.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
@@ -290,7 +304,7 @@ end
---@return string[] List of file references
function M.extract_file_references(content)
logger.func_entry("parser", "extract_file_references", { content_length = #content })
local files = {}
-- Pattern: @ followed by word char, dot, underscore, or dash as FIRST char
-- Then optionally more path characters including /
@@ -301,10 +315,10 @@ function M.extract_file_references(content)
logger.debug("parser", "extract_file_references: found file reference: " .. file)
end
end
logger.debug("parser", "extract_file_references: found " .. #files .. " file references")
logger.func_exit("parser", "extract_file_references", files)
return files
end
@@ -313,14 +327,14 @@ end
---@return string Cleaned content without file references
function M.strip_file_references(content)
logger.func_entry("parser", "strip_file_references", { content_length = #content })
-- Remove @filename patterns but preserve @/ closing tag
-- Pattern requires first char after @ to be word char, dot, underscore, or dash (NOT /)
local result = content:gsub("@([%w%._%-][%w%._%-/]*)", "")
logger.debug("parser", "strip_file_references: stripped " .. (#content - #result) .. " chars")
logger.func_exit("parser", "strip_file_references", "length=" .. #result)
return result
end
@@ -330,17 +344,16 @@ end
---@return number|nil start_line Line where the open tag starts
function M.is_cursor_in_open_tag(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
logger.func_entry("parser", "is_cursor_in_open_tag", { bufnr = bufnr })
local config = get_config_safe()
local cursor = vim.api.nvim_win_get_cursor(0)
local cursor_line = cursor[1]
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, cursor_line, false)
local escaped_open = utils.escape_pattern(config.patterns.open_tag)
local escaped_close = utils.escape_pattern(config.patterns.close_tag)
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 = 0
local close_count = 0
@@ -361,10 +374,20 @@ function M.is_cursor_in_open_tag(bufnr)
end
local is_inside = open_count > close_count
logger.debug("parser", "is_cursor_in_open_tag: open=" .. open_count .. ", close=" .. close_count .. ", is_inside=" .. tostring(is_inside) .. ", last_open_line=" .. tostring(last_open_line))
logger.debug(
"parser",
"is_cursor_in_open_tag: open="
.. open_count
.. ", close="
.. close_count
.. ", is_inside="
.. tostring(is_inside)
.. ", last_open_line="
.. tostring(last_open_line)
)
logger.func_exit("parser", "is_cursor_in_open_tag", { is_inside = is_inside, last_open_line = last_open_line })
return is_inside, is_inside and last_open_line or nil
end
@@ -373,7 +396,7 @@ end
---@return string|nil prefix The text after @ being typed, or nil if not typing a file ref
function M.get_file_ref_prefix(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
logger.func_entry("parser", "get_file_ref_prefix", { bufnr = bufnr })
local cursor = vim.api.nvim_win_get_cursor(0)
@@ -400,7 +423,7 @@ function M.get_file_ref_prefix(bufnr)
logger.debug("parser", "get_file_ref_prefix: prefix=" .. tostring(prefix))
logger.func_exit("parser", "get_file_ref_prefix", prefix)
return prefix
end

View File

@@ -6,8 +6,8 @@ local utils = require("codetyper.support.utils")
--- Patterns to add to .gitignore
local IGNORE_PATTERNS = {
"*.coder.*",
".coder/",
"*.codetyper/*",
".codetyper/",
}
--- Comment to identify codetyper entries
@@ -18,102 +18,102 @@ local CODER_COMMENT = "# Codetyper.nvim - AI coding partner files"
---@param pattern string Pattern to check
---@return boolean
local function pattern_exists(content, pattern)
local escaped = utils.escape_pattern(pattern)
return content:match("\n" .. escaped .. "\n") ~= nil
or content:match("^" .. escaped .. "\n") ~= nil
or content:match("\n" .. escaped .. "$") ~= nil
or content == pattern
local escaped = utils.escape_pattern(pattern)
return content:match("\n" .. escaped .. "\n") ~= nil
or content:match("^" .. escaped .. "\n") ~= nil
or content:match("\n" .. escaped .. "$") ~= nil
or content == pattern
end
--- Check if all patterns exist in gitignore content
---@param content string Gitignore content
---@return boolean, string[] All exist status and list of missing patterns
local function all_patterns_exist(content)
local missing = {}
for _, pattern in ipairs(IGNORE_PATTERNS) do
if not pattern_exists(content, pattern) then
table.insert(missing, pattern)
end
end
return #missing == 0, missing
local missing = {}
for _, pattern in ipairs(IGNORE_PATTERNS) do
if not pattern_exists(content, pattern) then
table.insert(missing, pattern)
end
end
return #missing == 0, missing
end
--- Get the path to .gitignore in project root
---@return string|nil Path to .gitignore or nil
function M.get_gitignore_path()
local root = utils.get_project_root()
if not root then
return nil
end
return root .. "/.gitignore"
local root = utils.get_project_root()
if not root then
return nil
end
return root .. "/.gitignore"
end
--- Check if coder files are already ignored
---@return boolean
function M.is_ignored()
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
return false
end
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
return false
end
local content = utils.read_file(gitignore_path)
if not content then
return false
end
local content = utils.read_file(gitignore_path)
if not content then
return false
end
local all_exist, _ = all_patterns_exist(content)
return all_exist
local all_exist, _ = all_patterns_exist(content)
return all_exist
end
--- Add coder patterns to .gitignore
---@return boolean Success status
function M.add_to_gitignore()
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
utils.notify("Could not determine project root", vim.log.levels.WARN)
return false
end
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
utils.notify("Could not determine project root", vim.log.levels.WARN)
return false
end
local content = utils.read_file(gitignore_path)
local patterns_to_add = {}
local content = utils.read_file(gitignore_path)
local patterns_to_add = {}
if content then
-- File exists, check which patterns are missing
local _, missing = all_patterns_exist(content)
if #missing == 0 then
return true -- All already ignored
end
patterns_to_add = missing
else
-- Create new .gitignore with all patterns
content = ""
patterns_to_add = IGNORE_PATTERNS
end
if content then
-- File exists, check which patterns are missing
local _, missing = all_patterns_exist(content)
if #missing == 0 then
return true -- All already ignored
end
patterns_to_add = missing
else
-- Create new .gitignore with all patterns
content = ""
patterns_to_add = IGNORE_PATTERNS
end
-- Build the patterns string
local patterns_str = table.concat(patterns_to_add, "\n")
-- Build the patterns string
local patterns_str = table.concat(patterns_to_add, "\n")
if content == "" then
-- New file
content = CODER_COMMENT .. "\n" .. patterns_str .. "\n"
else
-- Append to existing
local newline = content:sub(-1) == "\n" and "" or "\n"
-- Check if comment already exists
if not content:match(utils.escape_pattern(CODER_COMMENT)) then
content = content .. newline .. "\n" .. CODER_COMMENT .. "\n" .. patterns_str .. "\n"
else
content = content .. newline .. patterns_str .. "\n"
end
end
if content == "" then
-- New file
content = CODER_COMMENT .. "\n" .. patterns_str .. "\n"
else
-- Append to existing
local newline = content:sub(-1) == "\n" and "" or "\n"
-- Check if comment already exists
if not content:match(utils.escape_pattern(CODER_COMMENT)) then
content = content .. newline .. "\n" .. CODER_COMMENT .. "\n" .. patterns_str .. "\n"
else
content = content .. newline .. patterns_str .. "\n"
end
end
if utils.write_file(gitignore_path, content) then
utils.notify("Added coder patterns to .gitignore")
return true
else
utils.notify("Failed to update .gitignore", vim.log.levels.ERROR)
return false
end
if utils.write_file(gitignore_path, content) then
utils.notify("Added coder patterns to .gitignore")
return true
else
utils.notify("Failed to update .gitignore", vim.log.levels.ERROR)
return false
end
end
--- Ensure coder files are in .gitignore (called on setup)
@@ -122,115 +122,116 @@ end
---@param auto_gitignore? boolean Override auto_gitignore setting (default: true)
---@return boolean Success status
function M.ensure_ignored(auto_gitignore)
-- Only add to gitignore if this is a git project
if not utils.is_git_project() then
return false -- Not a git project, skip
end
-- Only add to gitignore if this is a git project
if not utils.is_git_project() then
return false -- Not a git project, skip
end
-- Default to true if not specified
if auto_gitignore == nil then
-- Try to get from config if available
local ok, codetyper = pcall(require, "codetyper")
if ok and codetyper.is_initialized and codetyper.is_initialized() then
local config = codetyper.get_config()
auto_gitignore = config and config.auto_gitignore
else
auto_gitignore = true -- Default to true
end
end
if not auto_gitignore then
return true
end
if not auto_gitignore then
return true
end
if M.is_ignored() then
return true
end
if M.is_ignored() then
return true
end
-- Default to true if not specified
if auto_gitignore == nil then
-- Try to get from config if available
local ok, codetyper = pcall(require, "codetyper")
if ok and codetyper.is_initialized and codetyper.is_initialized() then
local config = codetyper.get_config()
auto_gitignore = config and config.auto_gitignore
else
auto_gitignore = true -- Default to true
end
end
-- Silently add to gitignore (no notifications unless there's an error)
return M.add_to_gitignore_silent()
-- Silently add to gitignore (no notifications unless there's an error)
return M.add_to_gitignore_silent()
end
-- /@ @/
--- Add coder patterns to .gitignore silently (no notifications)
---@return boolean Success status
function M.add_to_gitignore_silent()
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
return false
end
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
return false
end
local content = utils.read_file(gitignore_path)
local patterns_to_add = {}
local content = utils.read_file(gitignore_path)
local patterns_to_add = {}
if content then
local _, missing = all_patterns_exist(content)
if #missing == 0 then
return true
end
patterns_to_add = missing
else
content = ""
patterns_to_add = IGNORE_PATTERNS
end
if content then
local _, missing = all_patterns_exist(content)
if #missing == 0 then
return true
end
patterns_to_add = missing
else
content = ""
patterns_to_add = IGNORE_PATTERNS
end
local patterns_str = table.concat(patterns_to_add, "\n")
local patterns_str = table.concat(patterns_to_add, "\n")
if content == "" then
content = CODER_COMMENT .. "\n" .. patterns_str .. "\n"
else
local newline = content:sub(-1) == "\n" and "" or "\n"
if not content:match(utils.escape_pattern(CODER_COMMENT)) then
content = content .. newline .. "\n" .. CODER_COMMENT .. "\n" .. patterns_str .. "\n"
else
content = content .. newline .. patterns_str .. "\n"
end
end
if content == "" then
content = CODER_COMMENT .. "\n" .. patterns_str .. "\n"
else
local newline = content:sub(-1) == "\n" and "" or "\n"
if not content:match(utils.escape_pattern(CODER_COMMENT)) then
content = content .. newline .. "\n" .. CODER_COMMENT .. "\n" .. patterns_str .. "\n"
else
content = content .. newline .. patterns_str .. "\n"
end
end
return utils.write_file(gitignore_path, content)
return utils.write_file(gitignore_path, content)
end
--- Remove coder patterns from .gitignore
---@return boolean Success status
function M.remove_from_gitignore()
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
return false
end
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
return false
end
local content = utils.read_file(gitignore_path)
if not content then
return false
end
local content = utils.read_file(gitignore_path)
if not content then
return false
end
-- Remove the comment and all patterns
content = content:gsub(CODER_COMMENT .. "\n", "")
for _, pattern in ipairs(IGNORE_PATTERNS) do
content = content:gsub(utils.escape_pattern(pattern) .. "\n?", "")
end
-- Remove the comment and all patterns
content = content:gsub(CODER_COMMENT .. "\n", "")
for _, pattern in ipairs(IGNORE_PATTERNS) do
content = content:gsub(utils.escape_pattern(pattern) .. "\n?", "")
end
-- Clean up extra newlines
content = content:gsub("\n\n\n+", "\n\n")
-- Clean up extra newlines
content = content:gsub("\n\n\n+", "\n\n")
return utils.write_file(gitignore_path, content)
return utils.write_file(gitignore_path, content)
end
--- Get list of patterns being ignored
---@return string[] List of patterns
function M.get_ignore_patterns()
return vim.deepcopy(IGNORE_PATTERNS)
return vim.deepcopy(IGNORE_PATTERNS)
end
--- Force update gitignore (manual trigger)
---@return boolean Success status
function M.force_update()
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
utils.notify("Could not determine project root for .gitignore", vim.log.levels.WARN)
return false
end
local gitignore_path = M.get_gitignore_path()
if not gitignore_path then
utils.notify("Could not determine project root for .gitignore", vim.log.levels.WARN)
return false
end
utils.notify("Updating .gitignore at: " .. gitignore_path)
return M.add_to_gitignore()
utils.notify("Updating .gitignore at: " .. gitignore_path)
return M.add_to_gitignore()
end
return M

View File

@@ -5,7 +5,7 @@ local M = {}
local utils = require("codetyper.support.utils")
--- Name of the coder folder
local CODER_FOLDER = ".coder"
local CODER_FOLDER = ".codetyper"
--- Name of the tree log file
local TREE_LOG_FILE = "tree.log"
@@ -23,8 +23,8 @@ local DEFAULT_SETTINGS = {
["workbench.colorTheme"] = "Default Dark+",
}
--- Get the path to the .coder folder
---@return string|nil Path to .coder folder or nil
--- Get the path to the .codetyper folder
---@return string|nil Path to .codetyper folder or nil
function M.get_coder_folder()
local root = utils.get_project_root()
if not root then
@@ -94,7 +94,7 @@ function M.ensure_settings()
return utils.write_file(settings_path, pretty_json)
end
--- Ensure .coder folder exists
--- Ensure .codetyper folder exists
---@return boolean Success status
function M.ensure_coder_folder()
local coder_folder = M.get_coder_folder()
@@ -212,12 +212,12 @@ function M.generate_tree()
"^node_modules$",
"^__pycache__$",
"^%.git$",
"^%.coder$",
"^%.codetyper$",
"^dist$",
"^build$",
"^target$",
"^vendor$",
"%.coder%.", -- Coder files
"%.codetyper%.", -- Coder files
}
local lines = {
@@ -242,7 +242,7 @@ end
--- Update the tree.log file
---@return boolean Success status
function M.update_tree_log()
-- Ensure .coder folder exists
-- Ensure .codetyper folder exists
if not M.ensure_coder_folder() then
return false
end
@@ -273,13 +273,13 @@ local function is_project_initialized(root)
end
--- Initialize tree logging (called on setup)
--- Only creates .coder/ folder for git projects (has .git/ folder)
--- Only creates .codetyper/ folder for git projects (has .git/ folder)
---@param force? boolean Force re-initialization even if cached
---@return boolean success
function M.setup(force)
-- Only initialize for git projects
if not utils.is_git_project() then
return false -- Not a git project, don't create .coder/
return false -- Not a git project, don't create .codetyper/
end
local coder_folder = M.get_coder_folder()
@@ -297,7 +297,7 @@ function M.setup(force)
return true
end
-- Ensure .coder folder exists (silent, no asking)
-- Ensure .codetyper folder exists (silent, no asking)
if not M.ensure_coder_folder() then
-- Silent failure - don't bother user
return false
@@ -338,7 +338,7 @@ function M.get_stats()
end
-- Skip hidden and special folders
if not name:match("^%.") and name ~= "node_modules" and not name:match("%.coder%.") then
if not name:match("^%.") and name ~= "node_modules" and not name:match("%.codetyper%.") then
if type == "directory" then
stats.directories = stats.directories + 1
count_recursive(path .. "/" .. name)

View File

@@ -56,30 +56,30 @@ end
---@param filepath string File path to check
---@return boolean
function M.is_coder_file(filepath)
return filepath:match("%.coder%.") ~= nil
return filepath:match("%.codetyper%.") ~= nil
end
--- Get the target file path from a coder file path
---@param coder_path string Path to the coder file
---@return string Target file path
function M.get_target_path(coder_path)
-- Convert index.coder.ts -> index.ts
return coder_path:gsub("%.coder%.", ".")
-- Convert index.codetyper/ts -> index.ts
return coder_path:gsub("%.codetyper%.", ".")
end
--- Get the coder file path from a target file path
---@param target_path string Path to the target file
---@return string Coder file path
function M.get_coder_path(target_path)
-- Convert index.ts -> index.coder.ts
-- Convert index.ts -> index.codetyper/ts
local dir = vim.fn.fnamemodify(target_path, ":h")
local name = vim.fn.fnamemodify(target_path, ":t:r")
local ext = vim.fn.fnamemodify(target_path, ":e")
if dir == "." then
return name .. ".coder." .. ext
return name .. ".codetyper/" .. ext
end
return dir .. "/" .. name .. ".coder." .. ext
return dir .. "/" .. name .. ".codetyper/" .. ext
end
--- Check if a file exists

View File

@@ -47,7 +47,7 @@
---@field end_col number Ending column
---@class CoderFile
---@field coder_path string Path to the .coder.* file
---@field coder_path string Path to the .codetyper/* file
---@field target_path string Path to the target file
---@field filetype string The filetype/extension