Ragex Usage Guide

View Source

Version: 0.2.0
Date: December 30, 2025

Complete guide to using Ragex, the Hybrid RAG system for intelligent codebase analysis and manipulation.


Table of Contents

  1. Quick Start
  2. MCP Integration
  3. How Do I Benefit From Ragex When Editing Elixir Project in VIM
  4. How Do I Benefit From Ragex When Editing Elixir Project in LunarVim (NeoVim)
  5. Core Workflows
  6. Code Analysis
  7. Semantic Search
  8. Code Editing
  9. Refactoring
  10. Configuration
  11. Best Practices
  12. Troubleshooting

Quick Start

Installation

# Clone the repository
git clone https://github.com/your-org/ragex.git
cd ragex

# Install dependencies
mix deps.get

# Compile the project
mix compile

First Run

# Start the Ragex MCP server
mix run --no-halt

# The server will:
# - Initialize the knowledge graph
# - Load the embedding model (first run downloads ~90MB)
# - Start listening on stdin/stdout for MCP commands

Quick Test

# In another terminal, use the MCP protocol:
echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | mix run --no-halt

MCP Integration

Ragex implements the Model Context Protocol (MCP) for seamless integration with AI assistants.

Connecting to Claude Desktop

Add to your Claude Desktop configuration (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):

{
  "mcpServers": {
    "ragex": {
      "command": "mix",
      "args": ["run", "--no-halt"],
      "cwd": "/path/to/ragex"
    }
  }
}

Available Tools

Ragex exposes 17 MCP tools across 4 categories:

Analysis Tools (5)

  • analyze_file - Parse and index source files
  • analyze_directory - Batch analyze entire projects
  • query_graph - Search for code entities
  • list_nodes - Browse indexed entities
  • watch_directory - Auto-reindex on file changes

Search Tools (4)

  • semantic_search - Natural language code search
  • hybrid_search - Combined symbolic + semantic search
  • get_embeddings_stats - ML model statistics
  • find_paths - Call chain discovery

Graph Tools (2)

  • graph_stats - Comprehensive analysis
  • list_watched - View watched directories

Editing Tools (6)

  • edit_file - Safe single-file editing
  • validate_edit - Preview validation
  • rollback_edit - Undo edits
  • edit_history - Query backups
  • edit_files - Multi-file transactions
  • refactor_code - Semantic refactoring

How Do I Benefit From Ragex When Editing Elixir Project in VIM

Ragex can significantly enhance your VIM workflow for Elixir development through several integration approaches.

1. Language Server Integration

Via ElixirLS + MCP Bridge:

Ragex complements ElixirLS by providing semantic code understanding that goes beyond traditional LSP capabilities.

" In your .vimrc or init.vim
" Configure ElixirLS first, then add Ragex integration

" Example function to query Ragex from VIM
function! RagexSemanticSearch(query)
  let l:cmd = 'echo ' . shellescape(json_encode({
        \ 'jsonrpc': '2.0',
        \ 'method': 'tools/call',
        \ 'params': {'name': 'semantic_search', 'arguments': {'query': a:query, 'limit': 10}},
        \ 'id': 1
        \ })) . ' | mix run --no-halt'
  let l:result = system(l:cmd)
  return json_decode(l:result)
endfunction

2. Code Navigation

Find definitions and usages semantically:

" Add to your VIM config
nnoremap <leader>rf :call RagexFindFunction()<CR>
nnoremap <leader>rc :call RagexFindCallers()<CR>
nnoremap <leader>rs :call RagexSearch(expand('<cword>'))<CR>

function! RagexFindFunction()
  " Get function under cursor and find its definition
  let l:word = expand('<cword>')
  " Query Ragex graph for function definition
  " Open result in quickfix window
endfunction

Benefits:

  • Semantic search beyond grep/ctags
  • Find functions by behavior description
  • Discover call chains between functions
  • Navigate complex codebases faster

3. Refactoring Support

Safe rename operations:

" Rename function across entire project
command! -nargs=1 RagexRename call RagexRenameFunction(<f-args>)

function! RagexRenameFunction(new_name)
  let l:module = RagexGetCurrentModule()
  let l:func = expand('<cword>')
  let l:arity = RagexGetFunctionArity()
  
  " Call Ragex refactor_code tool
  let l:request = {
        \ 'operation': 'rename_function',
        \ 'module': l:module,
        \ 'old_name': l:func,
        \ 'new_name': a:new_name,
        \ 'arity': l:arity,
        \ 'scope': 'project',
        \ 'validate': v:true,
        \ 'format': v:true
        \ }
  
  " Execute and reload affected buffers
endfunction

What you get:

  • Project-wide refactoring from VIM
  • Automatic validation and formatting
  • Rollback capability if issues occur
  • No partial updates (atomic transactions)

4. Intelligent Code Completion

Enhance completion with semantic context:

" Use with VIM's omnifunc or completion plugins
setlocal omnifunc=RagexComplete

function! RagexComplete(findstart, base)
  if a:findstart
    " Find start of completion
    return col('.') - 1
  else
    " Query Ragex for semantically relevant functions
    let l:context = getline('.')
    let l:results = RagexSemanticSearch('functions related to ' . a:base)
    return l:results
  endif
endfunction

5. Quick Documentation Lookup

" Show function details from knowledge graph
nnoremap K :call RagexShowDoc()<CR>

function! RagexShowDoc()
  let l:word = expand('<cword>')
  " Query Ragex for function details
  " Display in preview window with:
  " - Function signature
  " - File location
  " - Callers and callees
  " - Semantically similar functions
endfunction

6. Automated Workflows

Watch and auto-analyze on save:

" Auto-analyze file on save
autocmd BufWritePost *.ex,*.exs call RagexAnalyzeFile(expand('%:p'))

function! RagexAnalyzeFile(filepath)
  " Trigger Ragex analysis
  " Update knowledge graph
  " Regenerate embeddings for changed code
endfunction

7. Project Setup

Initial Ragex setup for VIM workflow:

# 1. Start Ragex in background (in project root)
cd /path/to/elixir/project
mix run --no-halt > /tmp/ragex.log 2>&1 &
echo $! > /tmp/ragex.pid

# 2. Initial analysis
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"analyze_directory","arguments":{"path":".","recursive":true,"extensions":[".ex",".exs"]}},"id":1}' | nc localhost 5555

# 3. Enable watching
echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"watch_directory","arguments":{"path":".","extensions":[".ex",".exs"]}},"id":2}' | nc localhost 5555

Add to your project's .vimrc.local:

" Project-specific Ragex configuration
let g:ragex_project_root = getcwd()
let g:ragex_enabled = 1

" Load Ragex VIM integration
source ~/.vim/ragex.vim

8. Integration with Existing Plugins

Works alongside:

  • ElixirLS: Traditional LSP features (diagnostics, basic completion)
  • vim-test: Run tests, Ragex finds related test files
  • fzf.vim: Combine fuzzy finding with semantic search
  • ALE/Syntastic: Linting + Ragex validation
  • vim-projectionist: Navigation + Ragex call graph

Example fzf integration:

" Semantic code search with fzf
command! -nargs=1 RagexFzf call fzf#run(fzf#wrap({
      \ 'source': RagexSearchSource(<q-args>),
      \ 'sink': function('RagexOpenResult'),
      \ 'options': '--preview "bat --color=always {1}"'
      \ }))

function! RagexSearchSource(query)
  " Query Ragex semantic search
  " Return list of file:line:content
endfunction

9. Performance Tips

Optimize for VIM workflow:

  1. Cache embeddings - Enable persistent cache for instant results
  2. Use async - Make Ragex calls asynchronous to avoid blocking VIM
  3. Limit results - Keep limit=10 for interactive use
  4. Filter by file scope - Search only relevant directories
" Async Ragex call example (with vim-dispatch or ALE)
function! RagexSearchAsync(query)
  call job_start(['mix', 'run', '--no-halt'], {
        \ 'in_io': 'pipe',
        \ 'out_cb': function('RagexHandleResults')
        \ })
endfunction

10. Sample VIM Plugin Structure

Create ~/.vim/ragex.vim:

" Ragex VIM Integration
" Author: Your Name
" Version: 0.1.0

if exists('g:loaded_ragex')
  finish
endif
let g:loaded_ragex = 1

" Configuration
let g:ragex_project_root = get(g:, 'ragex_project_root', getcwd())
let g:ragex_cmd = get(g:, 'ragex_cmd', 'mix run --no-halt')

" Core functions
function! ragex#search(query) abort
  " Implementation
endfunction

function! ragex#analyze_file(filepath) abort
  " Implementation
endfunction

function! ragex#find_callers() abort
  " Implementation
endfunction

" Commands
command! -nargs=1 RagexSearch call ragex#search(<f-args>)
command! RagexAnalyze call ragex#analyze_file(expand('%:p'))
command! RagexCallers call ragex#find_callers()

" Mappings
nnoremap <silent> <Plug>(ragex-search) :call ragex#search(expand('<cword>'))<CR>
nnoremap <silent> <Plug>(ragex-callers) :call ragex#find_callers()<CR>

Benefits Summary

Without Ragex:

  • Limited to grep, ctags, ElixirLS
  • No semantic understanding
  • Manual refactoring across files
  • Difficult to discover related code

With Ragex:

  • Natural language code search from VIM
  • Semantic navigation beyond syntax
  • Safe project-wide refactoring
  • Discover functions by behavior
  • Understand call chains instantly
  • Auto-updated knowledge graph
  • Rollback capability for edits

How Do I Benefit From Ragex When Editing Elixir Project in LunarVim (NeoVim)

LunarVim provides a modern NeoVim configuration with LSP, Telescope, and Lua integration, making Ragex integration even more powerful than traditional VIM.

1. Socket-Based Integration

Important: Ragex uses a Unix socket server for better performance and stability.

Start the server:

cd /path/to/ragex
./start_mcp.sh  # Starts server on /tmp/ragex_mcp.sock

Create ~/.config/lvim/lua/user/ragex.lua:

local M = {}

-- Configuration
M.config = {
  project_root = vim.fn.getcwd(),
  ragex_path = vim.fn.expand("~/Proyectos/Ammotion/ragex"),
  enabled = true,
  debug = false,  -- Set to true to see request/response logs
}

-- Log debug messages
local function debug_log(msg)
  if M.config.debug then
    vim.notify("[Ragex] " .. msg, vim.log.levels.INFO)
  end
end

-- Execute Ragex MCP command via Unix socket
function M.execute(method, params, callback)
  if not M.config.enabled then
    debug_log("Ragex is disabled")
    return
  end

  local request = vim.fn.json_encode({
    jsonrpc = "2.0",
    method = "tools/call",
    params = {
      name = method,
      arguments = params,
    },
    id = vim.fn.rand(),
  })

  debug_log("Request: " .. method)

  -- Use socat to communicate with Unix socket
  local cmd = string.format(
    "printf '%%s\\n' %s | socat - UNIX-CONNECT:/tmp/ragex_mcp.sock",
    vim.fn.shellescape(request)
  )

  if callback then
    -- Async execution
    vim.fn.jobstart(cmd, {
      stdout_buffered = true,
      on_stdout = function(_, data)
        if data and #data > 0 then
          local result_str = table.concat(data, "\n"):gsub("^%s+", ""):gsub("%s+$", "")
          if result_str ~= "" then
            debug_log("Response received")
            local ok, result = pcall(vim.fn.json_decode, result_str)
            if ok and result then
              callback(result)
            else
              vim.notify("Ragex: Invalid response format", vim.log.levels.WARN)
            end
          end
        end
      end,
      on_exit = function(_, exit_code)
        if exit_code ~= 0 then
          vim.notify("Ragex: Command failed (check if server is running)", vim.log.levels.WARN)
        end
      end,
    })
  else
    -- Synchronous execution
    local handle = io.popen(cmd)
    local result_str = ""
    if handle then
      result_str = handle:read("*a")
      handle:close()
    end
    
    if result_str and result_str ~= "" then
      result_str = result_str:gsub("^%s+", ""):gsub("%s+$", "")
      local ok, result = pcall(vim.fn.json_decode, result_str)
      if ok and result then
        return result
      end
    end
    return nil
  end
end

-- Semantic search
function M.semantic_search(query, opts)
  opts = opts or {}
  local params = {
    query = query,
    limit = opts.limit or 10,
    threshold = opts.threshold or 0.3,  -- Lower threshold for better recall
    node_type = opts.node_type,
  }

  return M.execute("semantic_search", params)
end

-- Hybrid search (best results)
function M.hybrid_search(query, opts)
  opts = opts or {}
  local params = {
    query = query,
    limit = opts.limit or 10,
    threshold = 0.3,  -- Lower threshold for better recall
    strategy = opts.strategy or "fusion",
  }

  return M.execute("hybrid_search", params)
end

-- Analyze current file
function M.analyze_current_file()
  local filepath = vim.fn.expand("%:p")
  return M.execute("analyze_file", { path = filepath })
end

-- Find callers of function under cursor
function M.find_callers()
  local module = M.get_current_module()
  local func = vim.fn.expand("<cword>")
  local arity = M.get_function_arity()

  return M.execute("query_graph", {
    query = string.format("calls %s.%s/%d", module, func, arity),
  })
end

-- Analyze directory
function M.analyze_directory(path, opts)
  opts = opts or {}
  path = path or vim.fn.getcwd()
  
  local params = {
    path = path,
    recursive = opts.recursive ~= false,
    parallel = opts.parallel ~= false,
    extensions = opts.extensions or { ".ex", ".exs" },
  }

  vim.notify("Analyzing directory: " .. path, vim.log.levels.INFO)
  
  M.execute("analyze_directory", params, function(result)
    if result and result.result then
      -- Unwrap MCP response
      local actual_result = result.result
      if actual_result.content and actual_result.content[1] and actual_result.content[1].text then
        local ok, parsed = pcall(vim.fn.json_decode, actual_result.content[1].text)
        if ok then
          actual_result = parsed
        end
      end
      
      local count = actual_result.analyzed or actual_result.success or 0
      local total = actual_result.total or 0
      vim.notify(string.format("Analyzed %d/%d files", count, total), vim.log.levels.INFO)
    else
      vim.notify("Failed to analyze directory", vim.log.levels.ERROR)
    end
  end)
end

-- Refactor: rename function
function M.rename_function(new_name, scope)
  local module = M.get_current_module()
  if not module then
    vim.notify("Could not determine current module", vim.log.levels.WARN)
    return
  end

  local old_name = vim.fn.expand("<cword>")
  local arity = M.get_function_arity()

  local params = {
    operation = "rename_function",
    params = {  -- Nested params object for MCP
      module = module,
      old_name = old_name,
      new_name = new_name,
      arity = arity,
    },
    scope = scope or "project",
    validate = true,
    format = true,
  }

  vim.notify(
    string.format("Renaming %s.%s/%d -> %s", module, old_name, arity, new_name),
    vim.log.levels.INFO
  )

  M.execute("refactor_code", params, function(result)
    if not result or not result.result then
      vim.notify("Refactoring failed: no response", vim.log.levels.ERROR)
      return
    end

    -- Unwrap MCP response
    local data = result.result
    if data.content and data.content[1] and data.content[1].text then
      local ok, parsed = pcall(vim.fn.json_decode, data.content[1].text)
      if ok then
        data = parsed
      end
    end

    if data.success then
      vim.cmd("checktime") -- Reload buffers
      vim.notify("Function renamed successfully", vim.log.levels.INFO)
    else
      local err = data.error or result.error or "unknown error"
      vim.notify("Refactoring failed: " .. vim.inspect(err), vim.log.levels.ERROR)
    end
  end)
end

-- Helper: Get current module name
function M.get_current_module()
  local lines = vim.api.nvim_buf_get_lines(0, 0, 50, false)
  for _, line in ipairs(lines) do
    local module = line:match("defmodule%s+([%w%.]+)")
    if module then
      return module
    end
  end
  return nil
end

-- Helper: Get function arity (simplified)
function M.get_function_arity()
  local line = vim.fn.getline(".")
  local args = line:match("def%s+%w+%((.-)%)")
  if not args or args == "" then
    return 0
  end
  local _, count = args:gsub(",", "")
  return count + 1
end

return M

2. Telescope Integration

Semantic code search with Telescope:

Add to ~/.config/lvim/config.lua:

local ragex = require("user.ragex")

-- Telescope picker for Ragex semantic search
local function ragex_search()
local pickers = require("telescope.pickers")
local finders = require("telescope.finders")
local conf = require("telescope.config").values
local actions = require("telescope.actions")
local action_state = require("telescope.actions.state")

  vim.ui.input({ prompt = "Ragex Search: " }, function(query)
    if not query then return end

    local results = ragex.hybrid_search(query)

    if not results.result or not results.result.results then
      vim.notify("No results found", vim.log.levels.WARN)
      return
    end

    local entries = {}
    for _, item in ipairs(results.result.results) do
      table.insert(entries, {
        value = item,
        display = string.format(
          "%s:%d [%.2f] %s",
          item.file or "unknown",
          item.line or 0,
          item.score or 0,
          item.text or ""
        ),
        ordinal = item.text or "",
        filename = item.file,
        lnum = item.line,
      })
    end

    pickers.new({}, {
      prompt_title = "Ragex Search Results",
      finder = finders.new_table({
        results = entries,
        entry_maker = function(entry)
          return entry
        end,
      }),
      sorter = conf.generic_sorter({}),
      attach_mappings = function(prompt_bufnr, map)
        actions.select_default:replace(function()
          actions.close(prompt_bufnr)
          local selection = action_state.get_selected_entry()
          if selection.filename then
            vim.cmd(string.format("edit +%d %s", selection.lnum, selection.filename))
          end
        end)
        return true
      end,
    }):find()
  end)
end

-- Register command
vim.api.nvim_create_user_command("RagexSearch", ragex_search, {})

3. Which-Key Integration

Add keybindings to LunarVim's which-key:

lvim.builtin.which_key.mappings["r"] = {
  name = "Ragex",
  s = { "<cmd>RagexSearch<cr>", "Semantic Search" },
  a = { function() require("user.ragex").analyze_current_file() end, "Analyze File" },
  c = { function() require("user.ragex").find_callers() end, "Find Callers" },
  r = {
    function()
      vim.ui.input({ prompt = "New name: " }, function(name)
        if name then
          require("user.ragex").rename_function(name)
        end
      end)
    end,
    "Rename Function",
  },
  f = { "<cmd>Telescope ragex_functions<cr>", "Find Functions" },
  m = { "<cmd>Telescope ragex_modules<cr>", "Find Modules" },
}

4. Custom Telescope Pickers

Create specialized Telescope pickers:

local ragex = require("user.ragex")
local pickers = require("telescope.pickers")
local finders = require("telescope.finders")
local conf = require("telescope.config").values

-- Find functions by semantic similarity
local function ragex_functions()
  vim.ui.input({ prompt = "Find functions: " }, function(query)
    if not query then return end

    local results = ragex.semantic_search(query, { node_type = "function" })

    if not results.result or not results.result.results then
      return
    end

    local entries = vim.tbl_map(function(item)
      return {
        value = item,
        display = string.format(
          "%s.%s/%d [%.2f]",
          item.node_id[1] or "?",
          item.node_id[2] or "?",
          item.node_id[3] or 0,
          item.score or 0
        ),
        ordinal = table.concat(item.node_id or {}, "."),
        filename = item.file,
        lnum = item.line,
      }
    end, results.result.results)

    pickers.new({}, {
      prompt_title = "Ragex Functions",
      finder = finders.new_table({ results = entries, entry_maker = function(e) return e end }),
      sorter = conf.generic_sorter({}),
      previewer = conf.qflist_previewer({}),
    }):find()
  end)
end

vim.api.nvim_create_user_command("RagexFunctions", ragex_functions, {})

5. LSP Integration

Enhance ElixirLS with Ragex:

-- Override LSP rename with Ragex refactoring
local function setup_lsp_overrides()
  lvim.lsp.on_attach_callback = function(client, bufnr)
    if client.name == "elixirls" then
      -- Use Ragex for rename instead of LSP
      vim.keymap.set("n", "<leader>lr", function()
        vim.ui.input({ prompt = "New name: " }, function(new_name)
          if new_name then
            require("user.ragex").rename_function(new_name)
          end
        end)
      end, { buffer = bufnr, desc = "Rename (Ragex)" })

      -- Add semantic search to LSP menu
      vim.keymap.set("n", "<leader>ls", function()
        require("user.ragex").semantic_search(vim.fn.expand("<cword>"))
      end, { buffer = bufnr, desc = "Semantic Search" })
    end
  end
end

setup_lsp_overrides()

6. Auto-Analysis on Save

Automatically analyze files when saving:

local ragex_group = vim.api.nvim_create_augroup("RagexAnalysis", { clear = true })

vim.api.nvim_create_autocmd({ "BufWritePost" }, {
  group = ragex_group,
  pattern = { "*.ex", "*.exs" },
  callback = function()
    -- Async analysis to avoid blocking
    vim.fn.jobstart(
      string.format(
        "cd %s && echo '%s' | mix run --no-halt",
        require("user.ragex").config.ragex_path,
        vim.fn.json_encode({
          jsonrpc = "2.0",
          method = "tools/call",
          params = {
            name = "analyze_file",
            arguments = { path = vim.fn.expand("%:p") },
          },
          id = vim.fn.rand(),
        })
      ),
      {
        on_exit = function(_, code)
          if code == 0 then
            vim.notify("File analyzed", vim.log.levels.DEBUG)
          end
        end,
      }
    )
  end,
})

7. Completion Integration

Add Ragex as a completion source:

local cmp = require("cmp")

local ragex_source = {}

function ragex_source:new()
  return setmetatable({}, { __index = self })
end

function ragex_source:get_trigger_characters()
  return { ".", ":" }
end

function ragex_source:complete(params, callback)
  local input = string.sub(params.context.cursor_before_line, params.offset)
  
  -- Query Ragex for semantic completions
  local results = require("user.ragex").semantic_search(
    "functions related to " .. input,
    { limit = 20, node_type = "function" }
  )

  if not results.result or not results.result.results then
    callback({})
    return
  end

  local items = vim.tbl_map(function(item)
    return {
      label = string.format("%s/%d", item.node_id[2], item.node_id[3]),
      kind = cmp.lsp.CompletionItemKind.Function,
      detail = item.text,
      documentation = string.format(
        "Module: %s\nFile: %s:%d\nScore: %.2f",
        item.node_id[1],
        item.file,
        item.line,
        item.score
      ),
    }
  end, results.result.results)

  callback(items)
end

-- Register the source
cmp.register_source("ragex", ragex_source:new())

-- Add to sources
table.insert(lvim.builtin.cmp.sources, { name = "ragex", priority = 750 })

8. Status Line Integration

Show Ragex status in LunarVim status line:

local function ragex_status()
  if not require("user.ragex").config.enabled then
    return ""
  end
  return "  Ragex"
end

-- Add to lualine
lvim.builtin.lualine.sections.lualine_x = {
  ragex_status,
  "encoding",
  "fileformat",
  "filetype",
}

9. Float Window for Results

Display search results in floating window:

local function show_in_float(title, lines)
  local buf = vim.api.nvim_create_buf(false, true)
  vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
  vim.api.nvim_buf_set_option(buf, "modifiable", false)

  local width = math.min(100, vim.o.columns - 4)
  local height = math.min(30, vim.o.lines - 4)

  local win = vim.api.nvim_open_win(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 = title,
    title_pos = "center",
  })

  vim.api.nvim_buf_set_keymap(buf, "n", "q", "<cmd>close<cr>", { noremap = true })
  vim.api.nvim_buf_set_keymap(buf, "n", "<Esc>", "<cmd>close<cr>", { noremap = true })
end

-- Show callers in float
function M.show_callers()
  local results = require("user.ragex").find_callers()
  
  if not results.result then
    vim.notify("No callers found", vim.log.levels.WARN)
    return
  end

  local lines = { "Callers:", "" }
  for _, caller in ipairs(results.result) do
    table.insert(lines, string.format("  %s:%d", caller.file, caller.line))
  end

  show_in_float("Ragex Callers", lines)
end

10. Full Configuration Example

Complete ~/.config/lvim/config.lua setup:

-- Ragex integration
local ragex = require("user.ragex")

-- Configure Ragex
ragex.config.ragex_path = vim.fn.expand("~/Proyectos/Ammotion/ragex")
ragex.config.enabled = true

-- Keybindings
lvim.builtin.which_key.mappings["r"] = {
  name = "Ragex",
  s = { "<cmd>RagexSearch<cr>", "Semantic Search" },
  f = { "<cmd>RagexFunctions<cr>", "Find Functions" },
  a = { function() ragex.analyze_current_file() end, "Analyze File" },
  c = { function() ragex.find_callers() end, "Find Callers" },
  r = {
    function()
      vim.ui.input({ prompt = "New name: " }, function(name)
        if name then ragex.rename_function(name) end
      end)
    end,
    "Rename Function",
  },
}

-- Auto-analysis
vim.api.nvim_create_autocmd({ "BufWritePost" }, {
  pattern = { "*.ex", "*.exs" },
  callback = function()
    ragex.analyze_current_file()
  end,
})

-- Status line (with safe initialization)
local function ragex_status()
  if ragex.config.enabled then
    return "  Ragex"
  end
  return ""
end

if lvim.builtin.lualine and lvim.builtin.lualine.sections and lvim.builtin.lualine.sections.lualine_x then
  table.insert(lvim.builtin.lualine.sections.lualine_x, 1, ragex_status)
else
  lvim.builtin.lualine = lvim.builtin.lualine or {}
  lvim.builtin.lualine.sections = lvim.builtin.lualine.sections or {}
  lvim.builtin.lualine.sections.lualine_x = lvim.builtin.lualine.sections.lualine_x or {}
  table.insert(lvim.builtin.lualine.sections.lualine_x, ragex_status)
end

11. Project-Specific Configuration

Create .lvimrc or .exrc in your Elixir project:

-- .lvimrc (requires exrc option)
local ragex = require("user.ragex")

-- Project-specific Ragex path
ragex.config.ragex_path = vim.fn.getcwd() .. "/../ragex"

-- Auto-analyze entire project on startup
vim.defer_fn(function()
  local result = ragex.execute("analyze_directory", {
    path = vim.fn.getcwd(),
    recursive = true,
    extensions = { ".ex", ".exs" },
  })
  
  if result.result then
    vim.notify(
      string.format("Analyzed %d files", result.result.files_analyzed or 0),
      vim.log.levels.INFO
    )
  end
end, 1000)

-- Enable watching
ragex.execute("watch_directory", {
  path = vim.fn.getcwd(),
  extensions = { ".ex", ".exs" },
})

Benefits Over Standard VIM

LunarVim/NeoVim advantages:

  • Lua API: Faster, more maintainable than VimScript
  • Async/Jobs: Non-blocking Ragex calls
  • Telescope: Beautiful search UI with previews
  • LSP Integration: Seamless with ElixirLS
  • Which-Key: Discoverable keybindings
  • Modern UI: Floating windows, notifications
  • Tree-sitter: Better syntax understanding
  • Built-in completion: Easy to extend with Ragex

Performance:

  • Async operations don't block editor
  • Telescope caching improves repeated searches
  • Lua is faster than VimScript
  • Job control for background analysis

Core Workflows

Workflow 1: Analyze a New Project

# Step 1: Start Ragex
mix run --no-halt

# Step 2: Analyze the project (via MCP)
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "name": "analyze_directory",
    "arguments": {
      "path": "/path/to/project",
      "recursive": true,
      "extensions": [".ex", ".exs", ".erl", ".py", ".js", ".ts"]
    }
  },
  "id": 1
}

Result:

  • All source files parsed
  • Functions, modules, calls indexed
  • Knowledge graph populated
  • Embeddings generated
  • Ready for search and analysis
# Natural language search
{
  "name": "semantic_search",
  "arguments": {
    "query": "function that validates user authentication",
    "limit": 10,
    "threshold": 0.7
  }
}

# Hybrid search (best results)
{
  "name": "hybrid_search",
  "arguments": {
    "query": "authentication validation",
    "limit": 10,
    "strategy": "fusion"
  }
}

Result:

  • Ranked list of relevant functions
  • Similarity scores
  • File locations
  • Function signatures

Workflow 3: Safe Code Refactoring

# Step 1: Rename a function across entire codebase
{
  "name": "refactor_code",
  "arguments": {
    "operation": "rename_function",
    "module": "MyApp.Auth",
    "old_name": "check_user",
    "new_name": "validate_user",
    "arity": 2,
    "scope": "project",
    "validate": true,
    "format": true
  }
}

What happens:

  1. Ragex finds function definition via graph
  2. Discovers all call sites
  3. Creates atomic transaction
  4. Updates all files
  5. Validates syntax in all files
  6. Formats code automatically
  7. Creates backups
  8. Commits changes atomically

If anything fails:

  • Automatic rollback
  • All files restored
  • No partial changes

Workflow 4: Multi-File Editing

{
  "name": "edit_files",
  "arguments": {
    "edits": [
      {
        "file": "lib/app/user.ex",
        "changes": [
          {
            "type": "replace",
            "start_line": 10,
            "end_line": 15,
            "new_content": "def new_implementation do\n  :ok\nend"
          }
        ],
        "validate": true,
        "format": true
      },
      {
        "file": "lib/app/admin.ex",
        "changes": [
          {
            "type": "insert",
            "line": 20,
            "content": "# New admin feature\n"
          }
        ]
      }
    ],
    "create_backup": true
  }
}

Safety guarantees:

  • All-or-nothing atomicity
  • Pre-validation of all files
  • Automatic rollback on any error
  • Backups created before changes
  • Format integration

Code Analysis

Analyzing Individual Files

Elixir:

# Via Elixir API
{:ok, analysis} = Ragex.Analyzers.Elixir.analyze(source_code, "lib/my_module.ex")

# Returns:
%{
  modules: [%{name: :MyModule, file: "lib/my_module.ex", line: 1}],
  functions: [
    %{module: :MyModule, name: :func, arity: 2, line: 10, file: "lib/my_module.ex"}
  ],
  calls: [
    %{from_module: :MyModule, from_function: :func, from_arity: 2,
      to_module: :OtherModule, to_function: :helper, to_arity: 1}
  ]
}

Python:

# Python analysis
content = """
def calculate_sum(a, b):
    return a + b

def main():
    result = calculate_sum(1, 2)
"""

{:ok, analysis} = Ragex.Analyzers.Python.analyze(content, "script.py")

Supported Languages:

  • Elixir (.ex, .exs) - Full AST parsing
  • Erlang (.erl, .hrl) - Full AST parsing
  • Python (.py) - AST via subprocess
  • JavaScript/TypeScript (.js, .jsx, .ts, .tsx, .mjs) - Regex-based

Batch Analysis

# Analyze entire directory
{
  "name": "analyze_directory",
  "arguments": {
    "path": "/path/to/project",
    "recursive": true,
    "parallel": true,
    "extensions": [".ex", ".exs"]
  }
}

Performance:

  • Parallel processing
  • ~100 files/second (depends on file size)
  • Progress reporting
  • Error handling per file

Watching for Changes

# Enable auto-reindex
{
  "name": "watch_directory",
  "arguments": {
    "path": "/path/to/project",
    "extensions": [".ex", ".exs"]
  }
}

Features:

  • Automatic reanalysis on file changes
  • Incremental updates (only changed files)
  • Embedding regeneration for modified entities
  • Real-time graph updates

Natural Language Queries

# Find authentication-related code
{
  "name": "semantic_search",
  "arguments": {
    "query": "user login and session management",
    "limit": 10,
    "threshold": 0.7,
    "node_type": "function"
  }
}

Response:

{
  "results": [
    {
      "node_type": "function",
      "node_id": ["MyApp.Auth", "authenticate_user", 2],
      "score": 0.92,
      "text": "Authenticates user credentials and creates session",
      "file": "lib/myapp/auth.ex",
      "line": 45
    }
  ]
}

Hybrid Search (Best Results)

{
  "name": "hybrid_search",
  "arguments": {
    "query": "validate email format",
    "limit": 10,
    "strategy": "fusion"  # or "semantic_first", "graph_first"
  }
}

Strategies:

  • fusion - Combines semantic + graph (RRF algorithm) - Recommended
  • semantic_first - Prioritizes ML similarity
  • graph_first - Prioritizes graph structure

Performance:

  • < 100ms typical query time
  • < 50ms for vector search
  • Scales to 10,000+ entities

Finding Call Chains

# Discover how functions are connected
{
  "name": "find_paths",
  "arguments": {
    "from": ["MyApp.Web", "handle_request", 2],
    "to": ["MyApp.DB", "save_record", 1],
    "max_depth": 5,
    "max_paths": 10
  }
}

Result:

  • All paths from function A to function B
  • Call chain depth
  • Intermediate functions
  • Useful for understanding data flow

Code Editing

Single File Editing

{
  "name": "edit_file",
  "arguments": {
    "file": "lib/my_module.ex",
    "changes": [
      {
        "type": "replace",
        "start_line": 10,
        "end_line": 12,
        "new_content": "def new_function do\n  :ok\nend"
      }
    ],
    "validate": true,
    "format": true,
    "create_backup": true
  }
}

Change Types:

  1. Replace:

    {
    "type": "replace",
    "start_line": 10,
    "end_line": 15,
    "new_content": "new code here"
    }
  2. Insert:

    {
    "type": "insert",
    "line": 20,
    "content": "new line\n"
    }
  3. Delete:

    {
    "type": "delete",
    "start_line": 10,
    "end_line": 12
    }

Validation

# Preview validation without editing
{
  "name": "validate_edit",
  "arguments": {
    "file": "lib/my_module.ex",
    "changes": [/* ... */],
    "language": "elixir"  # optional, auto-detected
  }
}

Validators:

Rollback

# Undo recent changes
{
  "name": "rollback_edit",
  "arguments": {
    "file": "lib/my_module.ex",
    "version": 2  # optional, defaults to previous
  }
}

# View backup history
{
  "name": "edit_history",
  "arguments": {
    "file": "lib/my_module.ex"
  }
}

Backup System:

  • Location: ~/.ragex/backups/<project_hash>/
  • Retention: 10 backups per file (configurable)
  • Compression: Automatic for older backups
  • Metadata: Timestamps, checksums

Formatting

Automatic formatting after edits:

  • Elixir: mix format
  • Erlang: rebar3 fmt
  • Python: black or autopep8
  • JavaScript: prettier or eslint --fix

Configuration:

# In edit request
"format": true,
"formatter": "mix format"  # optional, auto-detected

Refactoring

Rename Function

{
  "name": "refactor_code",
  "arguments": {
    "operation": "rename_function",
    "module": "MyApp.User",
    "old_name": "get_user",
    "new_name": "fetch_user",
    "arity": 1,
    "scope": "project",  # or "module"
    "validate": true,
    "format": true
  }
}

What gets updated:

  • Function definition
  • All direct calls
  • Module-qualified calls (MyApp.User.get_user(id))
  • Function references (&get_user/1)
  • Calls in other modules (if scope: "project")

Scope Options:

  • module - Only within the same file
  • project - Across entire codebase

Rename Module

{
  "name": "refactor_code",
  "arguments": {
    "operation": "rename_module",
    "old_name": "MyApp.OldModule",
    "new_name": "MyApp.NewModule",
    "validate": true,
    "format": true
  }
}

What gets updated:

  • Module definition
  • All aliases
  • All imports
  • All qualified calls
  • All references

Refactoring Workflow

  1. Discovery: Graph finds all affected files
  2. Planning: Builds transaction with all changes
  3. Validation: Pre-validates all files
  4. Backup: Creates backups
  5. Execution: Applies changes atomically
  6. Format: Runs formatters
  7. Verification: Post-validates all files
  8. Commit: Finalizes transaction

If any step fails:

  • Automatic rollback
  • All files restored
  • Error reported
  • No partial state

Current Limitations

  • Elixir only for AST-based refactoring
  • Erlang/Python/JavaScript support planned
  • No cross-language refactoring yet

Configuration

Embedding Models

Default: all-MiniLM-L6-v2 (384 dimensions)

Available Models:

# config/config.exs
config :ragex,
  embedding_model: :all_minilm_l6_v2  # Default, fast
  # embedding_model: :all_mpnet_base_v2  # Higher quality, 768 dims
  # embedding_model: :codebert_base  # Code-specific, 768 dims
  # embedding_model: :paraphrase_multilingual  # Multilingual, 384 dims

Or via environment variable:

export RAGEX_EMBEDDING_MODEL=all_mpnet_base_v2
mix run --no-halt

Cache Configuration

Enable persistent cache:

# config/config.exs
config :ragex,
  cache_embeddings: true,  # Default: false
  cache_dir: "~/.ragex/cache"

Benefits:

  • 10x faster cold start
  • ~15MB storage per 1,000 entities
  • Automatic invalidation on model change

Backup Configuration

config :ragex, :editor,
  backup_dir: "~/.ragex/backups",
  max_backups_per_file: 10,
  compress_old_backups: true

Performance Tuning

config :ragex,
  # Graph query limits
  max_paths: 100,
  max_path_depth: 10,
  
  # Search limits
  max_search_results: 100,
  
  # Parallel processing
  max_parallel_analyzers: System.schedulers_online() * 2

Best Practices

1. Project Setup

# Initial setup for a new project
1. Start Ragex
2. Analyze entire codebase
3. Enable directory watching
4. Let embeddings cache build

2. Incremental Development

# As you develop:
1. Keep directory watching enabled
2. Ragex auto-updates on file changes
3. Embeddings regenerate only for changed code
4. No manual reindexing needed

3. Search Strategies

For exploratory search:

  • Use hybrid_search with fusion strategy
  • Start with lower threshold (0.5)
  • Increase limit to see more results

For precise lookup:

  • Use query_graph for exact symbols
  • Use semantic_search with high threshold (0.8+)
  • Filter by node_type

4. Safe Refactoring

Before refactoring:

  1. Ensure codebase is analyzed
  2. Check graph stats
  3. Verify function/module exists

During refactoring:

  • Always use validate: true
  • Always use format: true
  • Use scope: "module" for testing
  • Use scope: "project" for production

After refactoring:

  • Verify files changed as expected
  • Run your test suite
  • Check edit history if needed
  • Rollback if issues found

5. Performance

For large codebases (>10,000 files):

  • Use parallel: true in analyze_directory
  • Enable caching
  • Consider incremental analysis only
  • Use path depth limits

For embedded systems:

  • Use smaller model (all-MiniLM-L6-v2)
  • Reduce max_search_results
  • Disable caching if storage limited

6. Error Handling

If analysis fails:

  • Check file is valid source code
  • Verify language is supported
  • Check file permissions
  • Look for syntax errors

If search returns no results:

  • Lower threshold
  • Try different query phrasing
  • Check embeddings are generated
  • Verify codebase is analyzed

If refactoring fails:

  • Check function/module exists in graph
  • Verify all files are accessible
  • Check validation errors
  • Use rollback if needed

Troubleshooting

Model Download Issues

Problem: Model fails to download

Solution:

# Manual download
cd ~/.cache/huggingface/
wget https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/model.safetensors

# Or use different model
export RAGEX_EMBEDDING_MODEL=all_mpnet_base_v2

Memory Issues

Problem: High memory usage

Solutions:

# Use smaller model
config :ragex, embedding_model: :all_minilm_l6_v2  # 384 dims vs 768

# Reduce batch size
config :ragex, embedding_batch_size: 16  # default: 32

# Limit search results
config :ragex, max_search_results: 50

Slow Analysis

Problem: Directory analysis is slow

Solutions:

# Enable parallel processing
{"parallel": true}

# Analyze incrementally
{"recursive": false}  # Analyze one directory at a time

# Filter extensions
{"extensions": [".ex"]}  # Only Elixir files

Graph State Issues

Problem: Entities not found in graph

Solutions:

# Clear and rebuild
1. Stop Ragex
2. rm -rf ~/.ragex/cache
3. Start Ragex
4. Re-analyze project

Validation Errors

Problem: Edits fail validation

Solutions:

  1. Check syntax of new content
  2. Verify language is correct
  3. Test validation separately
  4. Check validator is installed (Python, Node.js)

Backup/Rollback Issues

Problem: Cannot rollback

Solutions:

# Check backup directory
ls ~/.ragex/backups/<project_hash>/

# Verify backup exists
{
  "name": "edit_history",
  "arguments": {"file": "lib/my_module.ex"}
}

# Manual restore if needed
cp ~/.ragex/backups/<project_hash>/my_module.ex.<timestamp> lib/my_module.ex

Advanced Usage

Custom MCP Clients

# Python MCP client example
import json
import subprocess

def call_ragex(method, params):
    request = {
        "jsonrpc": "2.0",
        "method": f"tools/call",
        "params": {
            "name": method,
            "arguments": params
        },
        "id": 1
    }
    
    proc = subprocess.Popen(
        ["mix", "run", "--no-halt"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        cwd="/path/to/ragex"
    )
    
    proc.stdin.write(json.dumps(request).encode())
    proc.stdin.close()
    
    response = json.loads(proc.stdout.readline())
    return response

# Usage
result = call_ragex("semantic_search", {
    "query": "authentication",
    "limit": 5
})

Programmatic API

# Direct Elixir API usage
alias Ragex.{Graph.Store, VectorStore, Editor}

# Analyze
{:ok, analysis} = Ragex.Analyzers.Elixir.analyze(code, file)
Ragex.store_analysis(analysis)

# Search
{:ok, embedding} = Ragex.Embeddings.Bumblebee.embed("find auth code")
results = VectorStore.search(embedding, limit: 10)

# Edit
transaction = Editor.Transaction.new(validate: true, format: true)
|> Editor.Transaction.add(file, changes)
{:ok, result} = Editor.Transaction.commit(transaction)

Support


Last Updated: December 30, 2025
Version: 0.2.0
License: MIT