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
+40
View File
@@ -0,0 +1,40 @@
# Claude Vim Filter Plugin
Transform selected text in Vim/Neovim using Claude Code's `claude -p` command.
## What It Does
Select any text in Vim, press a keybinding, type an instruction (like "fix syntax" or "improve variable names"), and Claude transforms it instantly. Text is passed safely via stdin, so all special characters, quotes, newlines, and unicode work without escaping.
## Install
```bash
cd /path/to/vimai
python3 install.py
```
The installer auto-detects Vim or Neovim and copies the plugin to the right location.
## Usage
1. **Select text** - Visual mode: `V` + arrow keys
2. **Press `<leader>a`** - Default leader is `,` (comma)
3. **Type instruction** - Describe what you want
4. **Press Enter** - Done
**Examples:**
```vim
fix syntax errors
add type hints
improve variable names
convert to uppercase
fix grammar
```
Or use the command directly:
```vim
:ClaudeFilter add docstring
```
That's it.
Executable
+323
View File
@@ -0,0 +1,323 @@
#!/usr/bin/env python3
"""
Claude Filter Vim Plugin - Automatic Installation Script
Detects Vim/Neovim installation and installs the plugin with proper handling
for existing installations (updates/overrides).
Features:
- Auto-detects Vim vs Neovim
- Auto-detects config directory
- Installs both Lua (Neovim) and VimScript (Vim) versions
- Handles existing installations (updates)
- Cross-platform (Linux, macOS, Windows)
"""
import os
import sys
import shutil
import argparse
from pathlib import Path
class PluginInstaller:
def __init__(self, verbose=False, override=False):
self.verbose = verbose
self.override = override
self.installed = []
self.errors = []
def log(self, message, level="INFO"):
"""Log messages if verbose mode is enabled"""
if self.verbose or level in ["ERROR", "SUCCESS"]:
prefix = "✅" if level == "SUCCESS" else "❌" if level == "ERROR" else "ℹ️"
print(f"{prefix} [{level}] {message}")
def check_editor_installed(self):
"""Check which editors are installed"""
editors = {}
# Check Neovim
if shutil.which("nvim"):
editors["neovim"] = True
self.log("Neovim detected")
else:
editors["neovim"] = False
self.log("Neovim not found", "WARN")
# Check Vim
if shutil.which("vim"):
editors["vim"] = True
self.log("Vim detected")
else:
editors["vim"] = False
self.log("Vim not found", "WARN")
return editors
def get_neovim_config_dir(self):
"""Get Neovim config directory (works on all platforms)"""
# Check XDG_CONFIG_HOME first (Linux/macOS)
xdg_config = os.environ.get("XDG_CONFIG_HOME")
if xdg_config:
nvim_dir = Path(xdg_config) / "nvim"
if nvim_dir.exists():
return nvim_dir
# Check standard locations
home = Path.home()
# macOS
if sys.platform == "darwin":
nvim_dir = home / ".config" / "nvim"
if nvim_dir.exists():
return nvim_dir
# Linux
if sys.platform.startswith("linux"):
nvim_dir = home / ".config" / "nvim"
if nvim_dir.exists():
return nvim_dir
# Windows
if sys.platform == "win32":
# Try AppData first
appdata = os.environ.get("APPDATA")
if appdata:
nvim_dir = Path(appdata) / "nvim"
if nvim_dir.exists():
return nvim_dir
# Try local AppData
local_appdata = os.environ.get("LOCALAPPDATA")
if local_appdata:
nvim_dir = Path(local_appdata) / "nvim"
if nvim_dir.exists():
return nvim_dir
# Fallback: Create in ~/.config/nvim
return home / ".config" / "nvim"
def get_vim_config_dir(self):
"""Get Vim config directory"""
home = Path.home()
# Check for .vim directory
vim_dir = home / ".vim"
if vim_dir.exists():
return vim_dir
# Windows: check vimfiles
if sys.platform == "win32":
vimfiles = home / "vimfiles"
if vimfiles.exists():
return vimfiles
# Default to .vim
return vim_dir
def install_neovim_plugin(self):
"""Install Lua version for Neovim"""
self.log("Installing Neovim plugin...")
nvim_dir = self.get_neovim_config_dir()
plugin_dir = nvim_dir / "plugin"
# Create directory if it doesn't exist
plugin_dir.mkdir(parents=True, exist_ok=True)
# Source and destination
source = Path(__file__).parent / "plugin" / "claude_filter.lua"
destination = plugin_dir / "claude_filter.lua"
if not source.exists():
self.log(f"Source file not found: {source}", "ERROR")
self.errors.append(f"Neovim: Source file not found")
return False
# Check if already installed
if destination.exists():
if not self.override:
self.log(f"Already installed at {destination}")
self.log("Use --override to update")
return True
else:
self.log(f"Removing existing: {destination}")
destination.unlink()
# Copy file
try:
shutil.copy2(source, destination)
self.log(f"✅ Installed to {destination}", "SUCCESS")
self.installed.append(("Neovim", destination))
return True
except Exception as e:
self.log(f"Failed to copy: {e}", "ERROR")
self.errors.append(f"Neovim: {str(e)}")
return False
def install_vim_plugin(self):
"""Install VimScript version for Vim"""
self.log("Installing Vim plugin...")
vim_dir = self.get_vim_config_dir()
plugin_dir = vim_dir / "plugin"
# Create directory if it doesn't exist
plugin_dir.mkdir(parents=True, exist_ok=True)
# Source and destination
source = Path(__file__).parent / "plugin" / "claude_filter.vim"
destination = plugin_dir / "claude_filter.vim"
if not source.exists():
self.log(f"Source file not found: {source}", "ERROR")
self.errors.append(f"Vim: Source file not found")
return False
# Check if already installed
if destination.exists():
if not self.override:
self.log(f"Already installed at {destination}")
self.log("Use --override to update")
return True
else:
self.log(f"Removing existing: {destination}")
destination.unlink()
# Copy file
try:
shutil.copy2(source, destination)
self.log(f"✅ Installed to {destination}", "SUCCESS")
self.installed.append(("Vim", destination))
return True
except Exception as e:
self.log(f"Failed to copy: {e}", "ERROR")
self.errors.append(f"Vim: {str(e)}")
return False
def install(self):
"""Main installation logic"""
self.log("=" * 60)
self.log("Claude Filter Vim Plugin - Installer", "SUCCESS")
self.log("=" * 60)
# Check what's installed
editors = self.check_editor_installed()
if not any(editors.values()):
self.log("Neither Vim nor Neovim found on this system", "ERROR")
self.log("Install one of them first:")
self.log(" - Neovim: https://neovim.io")
self.log(" - Vim: https://www.vim.org")
return False
success = True
# Install for Neovim
if editors["neovim"]:
if not self.install_neovim_plugin():
success = False
# Install for Vim
if editors["vim"]:
if not self.install_vim_plugin():
success = False
# Summary
self.log("=" * 60)
if self.installed:
self.log("Installation Summary:", "SUCCESS")
for editor, path in self.installed:
self.log(f" {editor}: {path}")
if self.errors:
self.log("Errors encountered:", "ERROR")
for error in self.errors:
self.log(f" - {error}")
self.log("=" * 60)
# Usage instructions
if self.installed:
self.log("\n✅ Installation successful!\n", "SUCCESS")
self.log("Usage:")
self.log(" 1. Select text in visual mode (V + arrows)")
self.log(" 2. Press <leader>a (usually comma+a)")
self.log(" 3. Enter your instruction")
self.log(" 4. Text is transformed instantly")
self.log("\nDocumentation:")
self.log(" - Quick start: QUICK_START.md")
self.log(" - Detailed setup: SETUP.md")
self.log(" - Test cases: TEST_CASES.md")
self.log(" - Technical details: TECHNICAL.md")
return True
else:
self.log("Installation failed", "ERROR")
return False
def verify_installation(self):
"""Verify that plugin files exist and are readable"""
self.log("\nVerifying installation...")
all_good = True
for editor, path in self.installed:
if path.exists() and path.is_file():
size = path.stat().st_size
self.log(f"✓ {editor} plugin verified ({size} bytes)")
else:
self.log(f"✗ {editor} plugin not found at {path}", "ERROR")
all_good = False
return all_good
def main():
parser = argparse.ArgumentParser(
description="Install Claude Filter Vim plugin",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python3 install.py # Install with auto-detection
python3 install.py --verbose # Show detailed output
python3 install.py --override # Update existing installation
python3 install.py -v -o # Both verbose and override
"""
)
parser.add_argument(
"-v", "--verbose",
action="store_true",
help="Show detailed installation messages"
)
parser.add_argument(
"-o", "--override",
action="store_true",
help="Override existing installation (update)"
)
parser.add_argument(
"--verify",
action="store_true",
help="Only verify existing installation"
)
args = parser.parse_args()
installer = PluginInstaller(verbose=args.verbose, override=args.override)
if args.verify:
installer.install()
installer.verify_installation()
else:
success = installer.install()
if success:
installer.verify_installation()
sys.exit(0)
else:
sys.exit(1)
if __name__ == "__main__":
main()
+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>