I Built a Claude Code Context Modal Inside of Neovim
作者将AI助手Kai集成到Neovim中,创建了一个插件以实现更高效的代码处理。该插件通过分析用户的指令来决定是替换选中文本、插入新内容还是显示分析结果,并始终提供全面的上下文支持精准修改。 2025-7-26 15:30:0 Author: danielmiessler.com(查看原文) 阅读量:0 收藏

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.

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.

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 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.

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

bash

#!/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 $?

All right, that should get you started with the structure for your own implementation.


文章来源: https://danielmiessler.com/blog/neovim-claude-ai-plugin?utm_source=rss&utm_medium=feed&utm_campaign=website
如有侵权请联系:admin#unsafe.sh