WOrking perfect.

This commit is contained in:
2026-09-16 07:40:02 +00:00
commit cbee7d87b8
4 changed files with 691 additions and 0 deletions
+211
View File
@@ -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
+117
View File
@@ -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>