Files
codetyper.nvim/lua/codetyper/utils.lua
Carlos Gutierrez 60577f8951 feat: add conflict resolution, linter validation, and SEARCH/REPLACE system
- Add git-style conflict resolution with visual diff highlighting
- Add buffer-local keymaps: co/ct/cb/cn for conflict resolution
- Add floating menu with auto-show after code injection
- Add linter validation that auto-checks LSP diagnostics after accepting code
- Add SEARCH/REPLACE block parsing with fuzzy matching
- Add new commands: CoderConflictMenu, CoderLintCheck, CoderLintFix
- Update README with complete keymaps reference and issue reporting guide
- Update CHANGELOG and llms.txt with full documentation
- Clean up code comments and documentation

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-16 09:00:35 -05:00

198 lines
5.5 KiB
Lua

---@mod codetyper.utils Utility functions for Codetyper.nvim
local M = {}
--- Get the project root directory
---@return string|nil Root directory path or nil if not found
function M.get_project_root()
-- Try to find common root indicators
local markers = { ".git", ".gitignore", "package.json", "Cargo.toml", "go.mod", "pyproject.toml" }
local current = vim.fn.getcwd()
for _, marker in ipairs(markers) do
local found = vim.fn.findfile(marker, current .. ";")
if found ~= "" then
return vim.fn.fnamemodify(found, ":p:h")
end
found = vim.fn.finddir(marker, current .. ";")
if found ~= "" then
-- For directories, :p:h gives the dir itself, so we need :p:h:h to get parent
return vim.fn.fnamemodify(found, ":p:h:h")
end
end
return current
end
--- Check if current working directory IS a git repository root
--- Only returns true if .git folder exists directly in cwd (not in parent)
---@return boolean
function M.is_git_project()
local cwd = vim.fn.getcwd()
local git_path = cwd .. "/.git"
-- Check if .git exists as a directory or file (for worktrees)
return vim.fn.isdirectory(git_path) == 1 or vim.fn.filereadable(git_path) == 1
end
--- Get git root directory (only if cwd is a git root)
---@return string|nil Git root or nil if not a git project
function M.get_git_root()
if M.is_git_project() then
return vim.fn.getcwd()
end
return nil
end
--- Check if a file is a coder file
---@param filepath string File path to check
---@return boolean
function M.is_coder_file(filepath)
return filepath:match("%.coder%.") ~= 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%.", ".")
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
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
end
return dir .. "/" .. name .. ".coder." .. ext
end
--- Check if a file exists
---@param filepath string File path to check
---@return boolean
function M.file_exists(filepath)
local stat = vim.loop.fs_stat(filepath)
return stat ~= nil
end
--- Read file contents
---@param filepath string File path to read
---@return string|nil Contents or nil if error
function M.read_file(filepath)
local file = io.open(filepath, "r")
if not file then
return nil
end
local content = file:read("*all")
file:close()
return content
end
--- Write content to file
---@param filepath string File path to write
---@param content string Content to write
---@return boolean Success status
function M.write_file(filepath, content)
local file = io.open(filepath, "w")
if not file then
return false
end
file:write(content)
file:close()
return true
end
--- Create directory if it doesn't exist
---@param dirpath string Directory path
---@return boolean Success status
function M.ensure_dir(dirpath)
if vim.fn.isdirectory(dirpath) == 0 then
return vim.fn.mkdir(dirpath, "p") == 1
end
return true
end
--- Notify user with proper formatting
---@param msg string Message to display
---@param level? number Vim log level (default: INFO)
function M.notify(msg, level)
level = level or vim.log.levels.INFO
vim.notify("[Codetyper] " .. msg, level)
end
--- Get buffer filetype
---@param bufnr? number Buffer number (default: current)
---@return string Filetype
function M.get_filetype(bufnr)
bufnr = bufnr or 0
return vim.bo[bufnr].filetype
end
--- Escape pattern special characters
---@param str string String to escape
---@return string Escaped string
function M.escape_pattern(str)
return str:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1")
end
--- Get visual selection text
--- Call this BEFORE leaving visual mode or use marks '< and '>
---@return table|nil Selection info {text: string, start_line: number, end_line: number, filepath: string} or nil
function M.get_visual_selection()
local mode = vim.fn.mode()
-- Get marks - works in visual mode or after visual selection
local start_line = vim.fn.line("'<")
local end_line = vim.fn.line("'>")
local start_col = vim.fn.col("'<")
local end_col = vim.fn.col("'>")
-- If marks are not set (both 0), return nil
if start_line == 0 and end_line == 0 then
return nil
end
local bufnr = vim.api.nvim_get_current_buf()
local lines = vim.api.nvim_buf_get_lines(bufnr, start_line - 1, end_line, false)
if #lines == 0 then
return nil
end
-- Handle visual line mode - get full lines
local text
if mode == "V" or mode == "\22" then -- Visual line or Visual block
text = table.concat(lines, "\n")
else
-- Character-wise visual mode - trim first and last line
if #lines == 1 then
text = lines[1]:sub(start_col, end_col)
else
lines[1] = lines[1]:sub(start_col)
lines[#lines] = lines[#lines]:sub(1, end_col)
text = table.concat(lines, "\n")
end
end
local filepath = vim.fn.expand("%:p")
local filename = vim.fn.expand("%:t")
return {
text = text,
start_line = start_line,
end_line = end_line,
filepath = filepath,
filename = filename,
language = vim.bo[bufnr].filetype,
}
end
return M