WOrking perfect.
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
-- Claude Filter Plugin for Neovim/Vim
|
||||
-- Transforms selected text via claude -p with user-provided prompts
|
||||
-- Usage: Select text in visual mode, press <leader>a, enter your instruction
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Configuration
|
||||
M.config = {
|
||||
model = "haiku", -- Model to use
|
||||
system_prompt = [[You are a helpful text transformation assistant.
|
||||
When transforming text:
|
||||
1. Only change what was requested
|
||||
2. Preserve formatting intent
|
||||
3. Return only the transformed text, no explanations]],
|
||||
}
|
||||
|
||||
--- Initialize with custom options
|
||||
function M.setup(opts)
|
||||
M.config = vim.tbl_extend("force", M.config, opts or {})
|
||||
end
|
||||
|
||||
--- Get visual selection boundaries
|
||||
--- Returns: text, start_line, end_line, start_col, end_col
|
||||
local function get_visual_selection()
|
||||
-- Mark positions: '< is start, '> is end
|
||||
local s_start = vim.fn.getpos("'<")
|
||||
local s_end = vim.fn.getpos("'>")
|
||||
|
||||
if not s_start or not s_end then
|
||||
return nil, nil, nil, nil, nil
|
||||
end
|
||||
|
||||
local start_line = s_start[2]
|
||||
local start_col = s_start[3]
|
||||
local end_line = s_end[2]
|
||||
local end_col = s_end[3]
|
||||
|
||||
-- Get all lines in selection
|
||||
local lines = vim.fn.getline(start_line, end_line)
|
||||
if type(lines) == "string" then
|
||||
lines = { lines }
|
||||
end
|
||||
|
||||
-- Handle single-line selection
|
||||
if #lines == 1 then
|
||||
local text = string.sub(lines[1], start_col, end_col)
|
||||
return text, start_line, end_line, start_col, end_col
|
||||
end
|
||||
|
||||
-- Handle multi-line selection
|
||||
local text = string.sub(lines[1], start_col)
|
||||
for i = 2, #lines - 1 do
|
||||
text = text .. "\n" .. lines[i]
|
||||
end
|
||||
text = text .. "\n" .. string.sub(lines[#lines], 1, end_col)
|
||||
|
||||
return text, start_line, end_line, start_col, end_col
|
||||
end
|
||||
|
||||
--- Replace visual selection with new text (preserves surrounding text on same line)
|
||||
local function replace_selection(new_text, start_line, end_line, start_col, end_col)
|
||||
local split_lines = vim.split(new_text, "\n")
|
||||
|
||||
-- Single-line selection: preserve text before and after
|
||||
if start_line == end_line then
|
||||
local current_line = vim.fn.getline(start_line)
|
||||
local before = string.sub(current_line, 1, start_col - 1)
|
||||
local after = string.sub(current_line, end_col + 1)
|
||||
split_lines[1] = before .. split_lines[1] .. after
|
||||
end
|
||||
|
||||
-- Replace lines (vim uses 0-based indexing for nvim_buf_set_lines)
|
||||
vim.api.nvim_buf_set_lines(0, start_line - 1, end_line, false, split_lines)
|
||||
end
|
||||
|
||||
--- Execute claude -p with selected text via stdin
|
||||
--- @param text string The selected text
|
||||
--- @param prompt string The user's instruction
|
||||
--- @return string|nil Output text or nil on error
|
||||
--- @return string|nil Error message
|
||||
local function execute_claude(text, prompt)
|
||||
if not text or text == "" then
|
||||
return nil, "No text selected"
|
||||
end
|
||||
|
||||
if not prompt or prompt == "" then
|
||||
return nil, "No prompt provided"
|
||||
end
|
||||
|
||||
-- Build command with specified model
|
||||
local cmd = {
|
||||
"claude",
|
||||
"-p",
|
||||
prompt,
|
||||
"--model",
|
||||
M.config.model,
|
||||
}
|
||||
|
||||
-- If system prompt is set, add it
|
||||
if M.config.system_prompt and M.config.system_prompt ~= "" then
|
||||
table.insert(cmd, "--append-system-prompt")
|
||||
table.insert(cmd, M.config.system_prompt)
|
||||
end
|
||||
|
||||
-- Execute with stdin approach (SAFE for all special chars, newlines, etc)
|
||||
-- Text goes to stdin (2nd argument), not parsed by shell
|
||||
local output = vim.fn.system(cmd, text)
|
||||
|
||||
-- Check for errors
|
||||
if vim.v.shell_error ~= 0 then
|
||||
return nil, "Claude error: " .. output
|
||||
end
|
||||
|
||||
-- Remove trailing newline if present (claude may add one)
|
||||
if output:sub(-1) == "\n" then
|
||||
output = output:sub(1, -2)
|
||||
end
|
||||
|
||||
return output, nil
|
||||
end
|
||||
|
||||
--- Main filter function: prompt user and transform selection
|
||||
function M.filter_interactive()
|
||||
-- Get selection
|
||||
local selected_text, start_line, end_line, start_col, end_col = get_visual_selection()
|
||||
|
||||
if not selected_text or selected_text == "" then
|
||||
vim.notify("No text selected", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
-- Get prompt from user
|
||||
local prompt = vim.fn.input({
|
||||
prompt = "Enter instruction for Claude: ",
|
||||
cancelreturn = "",
|
||||
})
|
||||
|
||||
if prompt == "" then
|
||||
return
|
||||
end
|
||||
|
||||
-- Execute (no notifications - cleaner flow)
|
||||
local result, err = execute_claude(selected_text, prompt)
|
||||
|
||||
if err then
|
||||
vim.notify(err, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Replace selection (preserves surrounding text)
|
||||
replace_selection(result, start_line, end_line, start_col, end_col)
|
||||
end
|
||||
|
||||
--- Apply filter with pre-defined prompt (non-interactive)
|
||||
function M.filter(prompt)
|
||||
if not prompt or prompt == "" then
|
||||
vim.notify("Prompt cannot be empty", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
-- Get selection
|
||||
local selected_text, start_line, end_line, start_col, end_col = get_visual_selection()
|
||||
|
||||
if not selected_text or selected_text == "" then
|
||||
vim.notify("No text selected", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
-- Execute (no notifications - cleaner flow, no extra Enter)
|
||||
local result, err = execute_claude(selected_text, prompt)
|
||||
|
||||
if err then
|
||||
vim.notify(err, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
-- Replace selection (preserves surrounding text)
|
||||
replace_selection(result, start_line, end_line, start_col, end_col)
|
||||
end
|
||||
|
||||
--- Setup plugin: create commands and mappings
|
||||
function M.setup_mappings()
|
||||
-- Interactive command: prompts for instruction
|
||||
vim.api.nvim_create_user_command(
|
||||
"ClaudeFilter",
|
||||
function(opts)
|
||||
if opts.args and opts.args ~= "" then
|
||||
-- Called with argument: use it as prompt
|
||||
M.filter(opts.args)
|
||||
else
|
||||
-- Called without argument: prompt interactively
|
||||
M.filter_interactive()
|
||||
end
|
||||
end,
|
||||
{ nargs = "*", range = true }
|
||||
)
|
||||
|
||||
-- Visual mode mapping: press <leader>a to get interactive prompt
|
||||
vim.keymap.set(
|
||||
"v",
|
||||
"<leader>a",
|
||||
M.filter_interactive,
|
||||
{ noremap = true, silent = true, desc = "Claude filter (interactive)" }
|
||||
)
|
||||
end
|
||||
|
||||
-- Auto-setup if called
|
||||
M.setup()
|
||||
M.setup_mappings()
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,117 @@
|
||||
" Claude Filter Plugin - VimScript Version
|
||||
" For Vim users or those who prefer VimScript
|
||||
" Usage: Select text in visual mode, then :ClaudeFilter "your instruction"
|
||||
" Or press <leader>a and enter instruction at prompt
|
||||
|
||||
if exists('g:loaded_claude_filter_vim')
|
||||
finish
|
||||
endif
|
||||
let g:loaded_claude_filter_vim = 1
|
||||
|
||||
" Configuration
|
||||
let g:claude_filter_model = get(g:, 'claude_filter_model', 'haiku')
|
||||
let g:claude_filter_system_prompt = get(g:, 'claude_filter_system_prompt',
|
||||
\ "You are a helpful text transformation assistant.\n" .
|
||||
\ "When transforming text:\n" .
|
||||
\ "1. Only change what was requested\n" .
|
||||
\ "2. Preserve formatting intent\n" .
|
||||
\ "3. Return only the transformed text, no explanations")
|
||||
|
||||
" Main function: Apply Claude filter to selection with given prompt
|
||||
function! s:ApplyClaudeFilter(prompt) range
|
||||
if empty(a:prompt)
|
||||
echohl ErrorMsg
|
||||
echo 'Error: Prompt cannot be empty'
|
||||
echohl None
|
||||
return
|
||||
endif
|
||||
|
||||
" Get selection boundaries (handles partial-line selections)
|
||||
let start_pos = getpos("'<")
|
||||
let end_pos = getpos("'>")
|
||||
let start_line = start_pos[1]
|
||||
let start_col = start_pos[2]
|
||||
let end_line = end_pos[1]
|
||||
let end_col = end_pos[2]
|
||||
|
||||
" Get selected lines
|
||||
let lines = getline(start_line, end_line)
|
||||
|
||||
" Extract selected text (respecting column boundaries)
|
||||
let selected_text = ''
|
||||
if len(lines) == 1
|
||||
" Single line: extract substring between columns
|
||||
let selected_text = strpart(lines[0], start_col - 1, end_col - start_col + 1)
|
||||
else
|
||||
" Multiple lines: first line from col, middle lines full, last line to col
|
||||
let selected_text = strpart(lines[0], start_col - 1)
|
||||
for i in range(1, len(lines) - 2)
|
||||
let selected_text = selected_text . "\n" . lines[i]
|
||||
endfor
|
||||
let selected_text = selected_text . "\n" . strpart(lines[-1], 0, end_col)
|
||||
endif
|
||||
|
||||
if empty(selected_text)
|
||||
echohl ErrorMsg
|
||||
echo 'Error: No text selected'
|
||||
echohl None
|
||||
return
|
||||
endif
|
||||
|
||||
" Build command with model
|
||||
let cmd = 'claude -p ' . shellescape(a:prompt) . ' --model ' . shellescape(g:claude_filter_model)
|
||||
|
||||
" Add system prompt if set
|
||||
if !empty(g:claude_filter_system_prompt)
|
||||
let cmd = cmd . ' --append-system-prompt ' . shellescape(g:claude_filter_system_prompt)
|
||||
endif
|
||||
|
||||
" Execute via stdin (safe approach)
|
||||
" system() with two args: first is command, second is stdin input
|
||||
" Text goes to stdin, NOT parsed by shell - safe for all special chars
|
||||
let output = system(cmd, selected_text)
|
||||
|
||||
" Check for errors
|
||||
if v:shell_error != 0
|
||||
echohl ErrorMsg
|
||||
echo 'Claude error: ' . output
|
||||
echohl None
|
||||
return
|
||||
endif
|
||||
|
||||
" Remove trailing newline if present
|
||||
let output = substitute(output, '\n$', '', '')
|
||||
|
||||
" Split output into lines
|
||||
let output_lines = split(output, '\n')
|
||||
|
||||
" Replace selection, preserving surrounding text on same line
|
||||
if start_line == end_line
|
||||
" Single-line selection: preserve text before and after
|
||||
let current_line = getline(start_line)
|
||||
let before = strpart(current_line, 0, start_col - 1)
|
||||
let after = strpart(current_line, end_col)
|
||||
let output_lines[0] = before . output_lines[0] . after
|
||||
endif
|
||||
|
||||
" Delete old lines and insert new ones
|
||||
silent execute start_line . ',' . end_line . 'delete'
|
||||
call append(start_line - 1, output_lines)
|
||||
endfunction
|
||||
|
||||
" Command: :ClaudeFilter "your instruction"
|
||||
command! -range -nargs=+ ClaudeFilter <line1>,<line2>call s:ApplyClaudeFilter(<q-args>)
|
||||
|
||||
" Function for interactive mode (prompts user)
|
||||
function! s:InteractiveClaudeFilter() range
|
||||
let prompt = input('Enter instruction for Claude: ')
|
||||
if !empty(prompt)
|
||||
execute a:firstline . ',' . a:lastline . 'call s:ApplyClaudeFilter(prompt)'
|
||||
endif
|
||||
endfunction
|
||||
|
||||
" Command for interactive mode
|
||||
command! -range ClaudeFilterPrompt <line1>,<line2>call s:InteractiveClaudeFilter()
|
||||
|
||||
" Visual mode mapping: <leader>a
|
||||
vnoremap <leader>a :<C-u>ClaudeFilterPrompt<CR>
|
||||
Reference in New Issue
Block a user