I've been using AI to help with coding for a while now, but going back and forth between my code and AI was getting tedious—even with a highly optimized setup. So I integrated my Digital Assistant, Kai, directly into Neovim.
When working with AI for code editing, you typically want one of these actions:
Most AI integrations make you tell them exactly what to do. Kai somewhat figures it out from how you ask.
This entire thing is based on a capability in Claude Code that is massively underdiscussed. I just think of it as command-line mode.
claude -p
claude -p "What's the weather like in San Francisco right now?"
Anyway, that's what's going on under the hood. It's this command-line version of Claude Code that we're actually calling with this plug-in.
The plugin has two main pieces:
kai-neovim.lua
) - Handles Neovim integration, visual selections, and buffer managementkai-neovim.sh
) - Processes context and communicates with the AI backendThe plugin always sends the entire buffer as context, but intelligently focuses on:
This approach provides comprehensive context while enabling precise, targeted modifications based on your current selection or cursor position.
The plugin lets you basically tell it anything, and it tries to work it out. Here are some examples.
This is just completely insane. We can just send arbitrary things and have it kind of figure it out. Basically, command-line interaction with AI. And within your text editor.
This is the basic code for it, but keep in mind it's a work in progress. It might be total garbage. And yes, I had Kai help me build it for sure.
Save this as ~/.config/nvim/lua/kai-neovim.lua
:
local M = {}
-- Function to get visual selection
local function get_visual_selection()
-- Get the visual selection marks
local _, start_row, start_col, _ = unpack(vim.fn.getpos("'<"))
local _, end_row, end_col, _ = unpack(vim.fn.getpos("'>"))
-- Get the lines
local lines = vim.api.nvim_buf_get_lines(0, start_row - 1, end_row, false)
if #lines == 0 then
return ""
end
-- Handle single line selection
if #lines == 1 then
lines[1] = string.sub(lines[1], start_col, end_col)
else
-- Multi-line selection
lines[1] = string.sub(lines[1], start_col)
if end_col > 0 then
lines[#lines] = string.sub(lines[#lines], 1, end_col)
end
end
return table.concat(lines, "\n")
end
-- Function to escape special characters for shell
local function shell_escape(str)
return "'" .. str:gsub("'", "'\"'\"'") .. "'"
end
-- Main function to handle Kai Neovim integration
function M.kai_enhance()
-- Set up subtle blue highlight for the input prompt
vim.cmd('highlight KaiPrompt guifg=#e0e0e0 guibg=#1a1a2e')
-- Get the prompt from user with custom highlighting
vim.cmd('echohl KaiPrompt')
local prompt = vim.fn.input("🤖 Kai: ")
vim.cmd('echohl None')
if prompt == "" then
print("No instruction provided.")
return
end
-- Check if we're in visual mode
local mode = vim.fn.mode()
local is_visual = mode == 'v' or mode == 'V' or mode == ''
-- Get selection if in visual mode, empty string otherwise
local selection = ""
if is_visual then
selection = get_visual_selection()
end
-- Get current file path
local filepath = vim.fn.expand('%:p')
-- Get cursor position
local cursor_row, cursor_col = unpack(vim.api.nvim_win_get_cursor(0))
-- Get entire buffer content
local buffer_content = table.concat(vim.api.nvim_buf_get_lines(0, 0, -1, false), "\n")
-- Create a temporary file for the context
local context_file = os.tmpname()
local f = io.open(context_file, "w")
f:write("CURRENT FILE: " .. filepath .. "\n\n")
-- Always send the entire buffer
f:write("FULL BUFFER CONTENT:\n" .. buffer_content .. "\n\n")
-- Add cursor position
f:write("CURSOR POSITION: Line " .. cursor_row .. ", Column " .. cursor_col .. "\n\n")
if is_visual then
-- Include selection information when text is selected
local _, start_row, start_col, _ = unpack(vim.fn.getpos("'<"))
local _, end_row, end_col, _ = unpack(vim.fn.getpos("'>"))
f:write("SELECTED TEXT (Lines " .. start_row .. "-" .. end_row .. "):\n" .. selection .. "\n\n")
f:write("MODE: User has selected specific text. Focus on this selection within the context of the entire buffer.\n\n")
else
-- When no selection, note cursor position
f:write("MODE: No selection. User's cursor is at line " .. cursor_row .. ". Make targeted changes based on cursor location unless instructed otherwise.\n\n")
end
f:write("INSTRUCTION: " .. prompt .. "\n")
f:close()
-- Call Kai script
local cmd = string.format(
"~/.config/nvim/scripts/kai-neovim.sh %s %s",
shell_escape(context_file),
shell_escape(prompt)
)
-- Create progress notification (simplified for blog post)
print("🤖 Processing with Kai...")
-- Execute command
local output = vim.fn.system(cmd)
-- Clean up temp file
os.remove(context_file)
-- Parse the action and content from the response
local lines = vim.split(output, '\n', { plain = true })
local action = lines[1]
local content_lines = {}
for i = 2, #lines do
if lines[i] ~= "" or i < #lines then
table.insert(content_lines, lines[i])
end
end
local content = table.concat(content_lines, '\n')
-- Remove any trailing newline
content = content:gsub('\n$', '')
-- Handle different actions
if action == "[ACTION:DISPLAY]" then
-- Create a floating window to display the analysis
local display_buf = vim.api.nvim_create_buf(false, true)
local display_lines = vim.split(content, '\n', { plain = true })
-- Calculate window dimensions
local width = math.min(80, vim.o.columns - 10)
local height = math.min(#display_lines + 2, vim.o.lines - 10)
-- Set buffer content
vim.api.nvim_buf_set_lines(display_buf, 0, -1, false, display_lines)
-- Create floating window
local display_win = vim.api.nvim_open_win(display_buf, true, {
relative = 'editor',
width = width,
height = height,
col = math.floor((vim.o.columns - width) / 2),
row = math.floor((vim.o.lines - height) / 2),
style = 'minimal',
border = 'rounded',
title = ' Kai Analysis ',
title_pos = 'center',
})
-- Set up keymaps to close the window
local close_keys = {'<Esc>', 'q', '<CR>'}
for _, key in ipairs(close_keys) do
vim.api.nvim_buf_set_keymap(display_buf, 'n', key,
':lua vim.api.nvim_win_close(' .. display_win .. ', true)<CR>',
{ noremap = true, silent = true })
end
print("Kai analysis complete! Press <Esc>, q, or <Enter> to close.")
return
end
-- Perform the appropriate action based on the marker
if is_visual then
if action == "[ACTION:REPLACE]" then
-- Replace the selection
local save_reg = vim.fn.getreg('"')
local save_regtype = vim.fn.getregtype('"')
vim.fn.setreg('"', content, mode == 'V' and 'V' or 'v')
vim.cmd('normal! gv"_d') -- Delete selection without affecting registers
vim.cmd('normal! P') -- Paste before cursor
vim.fn.setreg('"', save_reg, save_regtype)
elseif action == "[ACTION:INSERT_AFTER]" then
-- Insert after the selection
vim.cmd('normal! gv') -- Reselect
vim.cmd('normal! o') -- Go to end of selection
vim.cmd('normal! ') -- Exit visual mode
-- Insert a newline and the content
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
local content_lines_new = vim.split(content, '\n', { plain = true })
-- Insert empty line first, then content
vim.api.nvim_buf_set_lines(0, row, row, false, {""})
vim.api.nvim_buf_set_lines(0, row + 1, row + 1, false, content_lines_new)
end
else
-- Normal mode - insert at cursor position
local content_lines_new = vim.split(content, '\n', { plain = true })
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
-- Insert the lines at cursor position
vim.api.nvim_buf_set_text(0, row - 1, col, row - 1, col, content_lines_new)
end
print("Kai enhancement complete!")
end
-- Set up the keymap
function M.setup()
-- Visual mode mapping
vim.keymap.set('v', '<leader>ai', M.kai_enhance,
{ noremap = true, silent = true, desc = "Enhance with Kai (intelligent action)" })
-- Normal mode mapping (insert at cursor)
vim.keymap.set('n', '<leader>ai', M.kai_enhance,
{ noremap = true, silent = true, desc = "Insert Kai text at cursor" })
end
return M
Create ~/.config/nvim/scripts/kai-neovim.sh
:
#!/bin/bash
# Kai Neovim Enhancement Script with Intelligent Action Detection
# Usage: kai-neovim.sh <context_file> <prompt>
CONTEXT_FILE="$1"
PROMPT="$2"
# Check if claude CLI is available (we use it to communicate with Kai)
if ! command -v claude &> /dev/null; then
echo "Error: Claude CLI not found. Please install it first."
exit 1
fi
# Read the CLAUDE.md files for additional context (project-specific rules for Kai)
GLOBAL_CLAUDE_MD=""
LOCAL_CLAUDE_MD=""
if [ -f "$HOME/.claude/CLAUDE.md" ]; then
GLOBAL_CLAUDE_MD=$(cat "$HOME/.claude/CLAUDE.md")
fi
# Find the nearest CLAUDE.md in the project
CURRENT_DIR=$(pwd)
while [ "$CURRENT_DIR" != "/" ]; do
if [ -f "$CURRENT_DIR/CLAUDE.md" ]; then
LOCAL_CLAUDE_MD=$(cat "$CURRENT_DIR/CLAUDE.md")
break
fi
CURRENT_DIR=$(dirname "$CURRENT_DIR")
done
# Regular text enhancement request - let Kai determine the action
FULL_PROMPT="You are Kai, an AI assistant integrated into Neovim.
CRITICAL CONTEXT FROM CLAUDE.md FILES (FOLLOW THESE RULES EXACTLY):
==================================================
GLOBAL CLAUDE.md:
$GLOBAL_CLAUDE_MD
PROJECT CLAUDE.md:
$LOCAL_CLAUDE_MD
==================================================
CURRENT EDITING CONTEXT:
$(cat "$CONTEXT_FILE")
CRITICAL: INTELLIGENT ACTION DETECTION
You must analyze the user's instruction to determine what they want:
1. If they say things like \"replace with\", \"change to\", \"rewrite as\", \"make this\", \"convert to\" → REPLACE the selected text
2. If they say things like \"write something like this\", \"create a note about\", \"add after\", \"insert\" → INSERT new content (don't replace)
3. If they say things like \"improve\", \"enhance\", \"fix\", \"correct\" → REPLACE with improved version
4. If they say things like \"explain this\", \"what is this\", \"analyze\", \"tell me about\", \"show me\", \"list\", \"count\", \"find\" → DISPLAY information (don't modify file)
IMPORTANT: When working with selected text, focus on that specific text within the context of the entire buffer. When working without selection, make targeted changes at the cursor location.
RESPONSE FORMAT:
You must start your response with ONE of these action markers on its own line:
[ACTION:REPLACE]
[ACTION:INSERT_AFTER]
[ACTION:INSERT_BEFORE]
[ACTION:DISPLAY]
Then on the next line, provide the content:
- For REPLACE/INSERT actions: provide ONLY the text to insert (no explanations)
- For DISPLAY actions: provide the analysis/information to show the user
IMPORTANT INSTRUCTIONS:
- First line must be the action marker
- Follow ALL formatting rules from CLAUDE.md
- Maintain the code style and conventions of the file
- Consider the context when generating content
- You are Kai, the AI assistant integrated into Neovim
User instruction: $PROMPT"
# Get the response with action marker
RESPONSE=$(echo "$FULL_PROMPT" | claude -p) # Using claude CLI to communicate with Kai
# Output the response
echo "$RESPONSE"
# Exit with the command's exit code
exit $?
Don't forget to make the script executable:
chmod +x ~/.config/nvim/scripts/kai-neovim.sh
All right, that should get you started with the structure for your own implementation.
And now you can talk directly to your AI from within Neovim!
vim.api.nvim_open_win()
), which is Neovim's implementation of modal-like popup windows that float above the main editor interface."