remvoe snacks; remove avante

This commit is contained in:
jmarkin 2025-11-26 02:23:32 +03:00
parent 448cdca018
commit f375a971b1
32 changed files with 469 additions and 4670 deletions

199
AGENTS.md Normal file
View file

@ -0,0 +1,199 @@
# Agent Guide for nvim-nix
This repository contains a Nix-based Neovim configuration with extensive plugin management and AI tooling integration.
## Project Overview
This is a **Nix-flaked Neovim configuration** that builds custom Neovim packages with pre-configured plugins and settings. The configuration emphasizes:
- **Nix package management** for reproducible builds
- **Extensive plugin ecosystem** with custom configurations
- **AI tooling integration** (Ollama, Airun, Gemini, etc.)
- **Multi-language support** with LSP configurations
- **Development-focused tooling** for coding workflows
## Essential Commands
### Nix Commands
```bash
# Build Neovim packages
nix build .#nvim # Full Neovim with all plugins
nix build .#nvim-small # Minimal Neovim with essential plugins
nix build .#nvim-minimal # Bare minimum Neovim
# Development shell with Lua/Nix tooling
nix develop # Enter devShell with lua-language-server, nixd, stylua, luacheck
# Update flake inputs
nix flake update # Update all plugin versions
# Install system-wide
nix profile install . # Install default (nvim) package
nix profile install .#nvim-small # Install minimal version
```
### Build/Development Commands
```bash
# Generate test data (from Makefile)
make tests-data # Downloads large JSON/YAML files for testing
# Lua code formatting
stylua <file.lua> # Format Lua files (config in .stylua.toml)
# Lua linting (in devShell)
luacheck <file.lua> # Check Lua syntax and style
```
## Directory Structure
```
nvim-nix/
├── nvim/ # Neovim configuration
│ ├── init.lua # Main configuration file
│ ├── plugin/ # Plugin configurations
│ ├── lua/ # Custom Lua modules
│ │ └── ai/ # AI tooling modules
│ └── after/ # After-load configurations
├── nix/ # Nix package definitions
│ ├── neovim-overlay.nix # Main overlay logic
│ ├── mkNeovim.nix # Neovim builder function
│ └── lang/ # Language-specific configs
├── tests/ # Test files and data
└── flake.nix # Nix flake definition
```
## Code Organization
### Neovim Configuration (`nvim/`)
- **`init.lua`**: Core configuration with global settings, options, and plugin loading
- **`plugin/`**: Individual plugin configurations (keymaps, settings, etc.)
- **`lua/`**: Custom Lua modules and utilities
- **`after/`**: After-load configurations and filetype plugins
### Plugin Structure
Plugins are organized by functionality:
- **AI tools**: `ai_avante.lua`, `ai/` directory with adapters and tools
- **Navigation**: `jumps.lua`, `window.lua`, `filemanager.lua`
- **Editing**: `format.lua`, `comment.lua`, `fold.lua`
- **UI**: `gui.lua`, `statusline.nix`, `colors/`
- **LSP**: `lspconfig.lua`, individual LSP configs in `lsp/`
### Nix Package Structure (`nix/`)
- **`neovim-overlay.nix`**: Main overlay that builds Neovim packages
- **`mkNeovim.nix`**: Builder function for creating Neovim derivations
- **`lang/`**: Language-specific plugin configurations
- Individual `.nix` files for specific features (treesitter, UI, etc.)
## Code Patterns and Conventions
### Lua Coding Style
- **Stylua formatting** (configured in `.stylua.toml`)
- 2-space indentation
- 120 column width
- Double quotes preferred
- Always use parentheses in function calls
- **Plugin loading pattern**:
```lua
if vim.g.did_load_plugin_name then return end
vim.g.did_load_plugin_name = true
-- plugin code here
```
- **Global variable usage**: Extensive use of `vim.g` for configuration
- **Option setting**: Use `vim.opt` for Neovim options
- **Autocommand patterns**: Use `vim.api.nvim_create_augroup` for grouping
### Nix Patterns
- **Plugin definitions**: Use `mkNvimPlugin` helper for building plugins
- **Overlay structure**: Follow the pattern in `neovim-overlay.nix`
- **Package variants**: Support `nvim`, `nvim-small`, `nvim-minimal`
- **Plugin categories**: `essential`, `coding`, `extra`, `ai-tools`
## Testing Approach
- **Test files**: Various language files in `tests/` directory for testing language support
- **Manual testing**: Primarily manual testing of Neovim functionality
- **Plugin testing**: Test individual plugin configurations
- **Build testing**: Verify Nix builds complete successfully
## AI Tooling Integration
### Supported AI Services
1. **Ollama** (local): Configured via `vim.g.ollama_*` variables
2. **Airun**: Remote AI service configuration
3. **Gemini**: Google AI integration
4. **OpenAI Compatible**: Generic API support
### AI Configuration Pattern
```lua
-- In ai/adapters.lua
M.service_name = function()
return require("codecompanion.adapters").extend("service_type", {
env = { /* environment variables */ },
opts = { /* service options */ },
schema = { /* model/configuration schema */ }
})
end
```
## Important Gotchas
### Plugin Loading Order
- Plugins use guard patterns to prevent double-loading
- Order matters for keymap and autocommand setup
- Some plugins depend on others being loaded first
### Nix Dependencies
- Requires specific Nixpkgs version (nixos-unstable)
- Some plugins may need additional system dependencies
- Build times can be long due to compilation
### Configuration Scope
- Global variables (`vim.g.*`) control most behavior
- Some settings are UI-dependent (terminal vs GUI)
- Plugin configurations can override global settings
### Development Workflow
- Use `nix develop` for consistent development environment
- Test builds with `nix build .#nvim-small` for faster iteration
- Update plugins via `nix flake update`
## Development Environment Setup
1. **Enter development shell**:
```bash
nix develop
```
2. **Link configuration for testing**:
```bash
# From devShell, config is automatically linked to ~/.config/nvim-dev
nvim -u ~/.config/nvim-dev/init.lua
```
3. **Format and lint**:
```bash
stylua nvim/**/*.lua
luacheck nvim/**/*.lua
```
## Project-Specific Context
This configuration is maintained by Jury Markin and includes:
- **Custom color schemes**: `ex-bamboo`, `ex-bluloco`, etc.
- **Personal workflow optimizations**: Specific keymaps and settings
- **bleeding-edge plugins**: Frequently updated via flake inputs
- **Multi-language development**: Support for Go, Python, Rust, JS, etc.
- **AI-assisted development**: Integrated tools for code generation and assistance
The configuration balances personal preferences with general usability, making it suitable for both the maintainer's workflow and as a reference for Nix-based Neovim setups.

160
README.md
View file

@ -0,0 +1,160 @@
# Конфигурация Neovim для Nix
Nix-flaked конфигурация Neovim с расширенным менеджментом плагинов и интеграцией AI-инструментов, поддерживаемая Jury Markin.
## Особенности
- **Nix-базированная упаковка** для воспроизводимых, декларативных сборок Neovim
- **Обширная экосистема плагинов** с кастомными конфигурациями
- **Интеграция AI-инструментов** (Ollama, Airun, Gemini, OpenAI-совместимые)
- **Многоязыковая поддержка** с комплексными LSP-конфигурациями
- **Разработанные инструменты** для современных рабочих процессов кодирования
- **Несколько вариантов сборки**: полная, малая и минимальная конфигурации
## Быстрый старт
### Предварительные требования
- Менеджер пакетов Nix с включенными flakes
- Современный Neovim (рекомендуется 0.11+ для полной поддержки функций)
### Установка
1. **Установите полную конфигурацию**:
```bash
nix profile install github:JMarkin/nvim-nix
```
2. **Установите минимальную версию** (рекомендуется для большинства пользователей):
```bash
nix profile install github:JMarkin/nvim-nix#nvim-small
```
3. **Для разработки/тестирования**, клонируйте и соберите локально:
```bash
git clone https://github.com/JMarkin/nvim-nix.git
cd nvim-nix
nix build .#nvim-small
result/bin/nvim # Запустите собранный Neovim
```
## Варианты пакетов
- **`nvim`**: Полная конфигурация со всеми плагинами и функциями
- **`nvim-small`**: Основные плагины и конфигурации (рекомендуется)
- **`nvim-minimal`**: Минимальная настройка для быстрого запуска
- **`codingPackages`**: Пакет инструментов разработки
## Основные особенности конфигурации
### Интеграция AI
Конфигурация включает расширенные AI-инструменты:
- **Ollama** интеграция для локальных LLM
- **Airun** поддержка удаленных AI-сервисов
- **Gemini** и **OpenAI-совместимая** поддержка API
- **CodeCompanion** интеграция для AI-ассистированной разработки
### Экосистема плагинов
Ключевые категории плагинов:
- **Поддержка языков**: LSP-конфигурации для Go, Python, Rust, JavaScript и других
- **Навигация**: Улучшенное перемещение, поиск и управление файлами
- **Редактирование**: Расширенное форматирование, комментирование, сворачивание и рефакторинг
- **UI/UX**: Кастомная строка состояния, цветовые схемы и улучшения интерфейса
- **Продуктивность**: Git-интеграция, управление терминалом и оптимизация рабочих процессов
### Особенности разработки
- **Встроенная поддержка LSP** с автоматическим обнаружением серверов
- **Treesitter интеграция** для продвинутого синтаксического парсинга
- **Многоязыковые среды разработки**
- **Кастомные ключевые карты** для оптимизации продуктивности
- **Управление сессиями** и навигация по проектам
## Разработка
### Development Shell
Зайдите в среду разработки со всеми необходимыми инструментами:
```bash
nix develop
```
Это предоставляет:
- `lua-language-server` для разработки на Lua
- `nixd` для разработки на Nix
- `stylua` для форматирования Lua-кода
- `luacheck` для Lua-линтинга
- Автоматическую привязку к `~/.config/nvim-dev` для тестирования
### Сборка
```bash
# Собрать все варианты
nix build .
# Собрать конкретные варианты
nix build .#nvim
nix build .#nvim-small
nix build .#nvim-minimal
# Обновить версии плагинов
nix flake update
```
### Тестирование конфигурации
Протестируйте изменения в режиме разработки:
```bash
# Из development shell
nvim -u ~/.config/nvim-dev/init.lua
```
## Настройка
### Добавление плагинов
Добавьте новые плагины в `nix/neovim-overlay.nix`:
1. Добавьте входные данные плагина в `flake.nix`
2. Создайте определение плагина с использованием `mkNvimPlugin`
3. Добавьте в соответствующий список плагинов (`essential`, `coding` и т.д.)
### Изменение конфигурации
- **Основная конфигурация**: `nvim/init.lua`
- **Конфигурация плагинов**: `nvim/plugin/*.lua`
- **Конфигурация языков**: `nix/lang/*.nix`
- **Конфигурация сборки**: `nix/mkNeovim.nix`
### AI-конфигурация
Настройте AI-сервисы в `nvim/lua/ai/adapters.lua`:
- Установите переменные окружения для API-ключей/эндпоинтов
- Настройте предпочтения моделей и опции
- Настройте специфические настройки сервисов
## Требования
- **Nix** с включенными flakes
- **Neovim 0.10+** (рекомендуется 0.11+)
- **Современный терминал** с поддержкой truecolor
- **Опционально**: LLM-бэкинги для AI-функций (Ollama и т.д.)
## Лицензия
MIT License - см. файл LICENSE для деталей.
## Вклад
Приветствуются вклады! Пожалуйста:
1. Тщательно тестируйте изменения
2. Следуйте существующему стилю кода (форматирование stylua)
3. Обновляйте документацию по мере необходимости
4. Учитывайте влияние на время сборки и зависимости
## Поддержка
- **Issues**: GitHub трекер задач
- **Документация**: [AGENTS.md](./agent.md) для детального руководства по разработке
- **Конфигурация**: Изучите `nvim/init.lua` и файлы плагинов для примеров

View file

@ -302,46 +302,6 @@
"type": "github"
}
},
"neovim-nightly-overlay": {
"inputs": {
"flake-parts": [
"flake-parts"
],
"neovim-src": "neovim-src",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1764029077,
"narHash": "sha256-a3imRMIsRjonDDyt4buoTaXnCJ0H6FSZEes0tHyWr0s=",
"owner": "nix-community",
"repo": "neovim-nightly-overlay",
"rev": "4e2ede6fae4af2d474f63028ff54bd714707c3e7",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "neovim-nightly-overlay",
"type": "github"
}
},
"neovim-src": {
"flake": false,
"locked": {
"lastModified": 1764025805,
"narHash": "sha256-DMG2kVggmBUbr2lxFugiRjiCBOkC9vU822JTkpYTeN4=",
"owner": "neovim",
"repo": "neovim",
"rev": "60c35cc4c7b713c27e8bfdd196cbee46cf050bbb",
"type": "github"
},
"original": {
"owner": "neovim",
"repo": "neovim",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1741865919,
@ -480,7 +440,6 @@
"kulala-nvim": "kulala-nvim",
"local-highlight-nvim": "local-highlight-nvim",
"namu-nvim": "namu-nvim",
"neovim-nightly-overlay": "neovim-nightly-overlay",
"nixpkgs": "nixpkgs_2",
"nvim-window": "nvim-window",
"nvim-yati": "nvim-yati",

View file

@ -1,7 +1,8 @@
{
description = "Neovim derivation";
description = "Nix-flaked Neovim configuration with extensive plugin management and AI tooling integration";
nixConfig = {
# Custom binary cache for faster builds
extra-substituters = [
"http://tln.jmarkin.ru:8501"
];
@ -11,28 +12,26 @@
};
inputs = {
# Core Nix infrastructure
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
neovim-nightly-overlay.url = "github:nix-community/neovim-nightly-overlay";
neovim-nightly-overlay.inputs.nixpkgs.follows = "nixpkgs";
neovim-nightly-overlay.inputs.flake-parts.follows = "flake-parts";
flake-parts.url = "github:hercules-ci/flake-parts";
gen-luarc.url = "github:mrcjkb/nix-gen-luarc-json";
gen-luarc.inputs.nixpkgs.follows = "nixpkgs";
gen-luarc.inputs.flake-parts.follows = "flake-parts";
# Theme and UI dependencies
blink-pairs.url = "github:Saghen/blink.pairs?ref=574ce24d44526a76e0b76e921a92c6737a6b3954";
# kulala-nvim ecosystem
# Development and formatting tools
kulala-nvim.url = "github:mistweaverco/kulala.nvim";
kulala-nvim.flake = false;
kulala-fmt.url = "github:mistweaverco/kulala-fmt";
kulala-fmt.inputs.flake-parts.follows = "flake-parts";
kulala-fmt.inputs.nixpkgs.follows = "nixpkgs";
# Add bleeding-edge plugins here.
# They can be updated with `nix flake update` (make sure to commit the generated flake.lock)
# Bleeding-edge Neovim plugins
# These can be updated with `nix flake update` (remember to commit flake.lock)
smart-splits-nvim = {
url = "github:mrjones2014/smart-splits.nvim";
@ -113,6 +112,7 @@
};
in
flake-parts.lib.mkFlake { inherit inputs; } {
# Supported systems - add more as needed
systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ];
perSystem = { system, pkgs, ... }:
{
@ -134,24 +134,29 @@
nvim-minimal = pkgs.nvim-minimal;
default = nvim;
# Development tools and language-specific packages
codingPackages = pkgs.codingPackages;
};
devShells = {
default = pkgs.mkShell {
name = "nvim-devShell";
buildInputs = with pkgs; [
# Tools for Lua and Nix development, useful for editing files in this repo
lua-language-server
nixd
stylua
luajitPackages.luacheck
nvim-dev
# Essential development tools for maintaining this flake
lua-language-server # Lua language server for config development
nixd # Nix language server
stylua # Lua code formatter
luajitPackages.luacheck # Lua linter
nvim-dev # Development version of Neovim
];
shellHook = ''
# symlink the .luarc.json generated in the overlay
# Symlink generated .luarc.json for IDE support
ln -fs ${pkgs.nvim-luarc-json} .luarc.json
# allow quick iteration of lua configs
# Link configuration for development testing
# This allows testing changes with: nvim -u ~/.config/nvim-dev/init.lua
ln -Tfns $PWD/nvim ~/.config/nvim-dev
echo "Test with: nvim -u ~/.config/nvim-dev/init.lua"
'';
};
};

View file

@ -1,24 +1,85 @@
{ inputs, pkgs, mkNvimPlugin, ... }:
with pkgs.vimPlugins; [
{
plugin = avante-nvim.overrideAttrs (oa: {
dependencies = with pkgs.vimPlugins; [
nui-nvim
nvim-treesitter
plenary-nvim
plugin = sidekick-nvim.overrideAttrs (oa: {
runtimeDeps = [
];
});
type = "lua";
optional = true;
}
{
plugin = blink-cmp-avante;
type = "lua";
optional = true;
config = /*lua*/''
lze.load {
"${blink-cmp-avante.pname}",
on_plugin = "blink.cmp",
"${sidekick-nvim.pname}",
cmd = {"Sidekick"},
after = function()
require("sidekick").setup {
nes = { enabled = false },
cli = {
mux = {
backend = "tmux",
enabled = true,
create = "split",
},
picker = "fzf-lua",
--- CLI Tool Keymaps (default mode is `t`)
---@type table<string, sidekick.cli.Keymap|false>
keys = {
buffers = { "<c-b>", "buffers" , mode = "nt", desc = "open buffer picker" },
files = { "<c-f>", "files" , mode = "nt", desc = "open file picker" },
hide_ctrl_q = { "<A-q>", "hide" , mode = "n" , desc = "hide the terminal window" },
hide_ctrl_dot = { "<c-.>", "hide" , mode = "nt", desc = "hide the terminal window" },
prompt = { "<c-p>", "prompt" , mode = "t" , desc = "insert prompt or context" },
},
},
}
end,
keys = {
{
"<c-.>",
function() require("sidekick.cli").toggle({name="crush"}) end,
desc = "Sidekick Toggle",
mode = { "n", "t", "i", "x" },
},
{
"<leader>aa",
function() require("sidekick.cli").toggle({name="crush"}) end,
desc = "Sidekick Toggle CLI",
},
{
"<leader>as",
function() require("sidekick.cli").select({ filter = { installed = true } }) end,
desc = "Select CLI",
},
{
"<leader>ad",
function() require("sidekick.cli").close() end,
desc = "Detach a CLI Session",
},
{
"<leader>at",
function() require("sidekick.cli").send({ msg = "{this}" }) end,
mode = { "x", "n" },
desc = "Send This",
},
{
"<leader>af",
function() require("sidekick.cli").send({ msg = "{file}" }) end,
desc = "Send File",
},
{
"<leader>av",
function() require("sidekick.cli").send({ msg = "{selection}" }) end,
mode = { "x" },
desc = "Send Visual Selection",
},
{
"<leader>ap",
function() require("sidekick.cli").prompt() end,
mode = { "n", "x" },
desc = "Sidekick Select Prompt",
},
},
}
'';
}

View file

@ -7,8 +7,6 @@
, # Set by the overlay to ensure we use a compatible version of `wrapNeovimUnstable`
wrapNeovimUnstable
, neovimUtils
, neovim-nightly
,
}:
with lib;
{

View file

@ -20,12 +20,10 @@ with final.pkgs.lib; let
# Make sure we use the pinned nixpkgs instance for wrapNeovimUnstable,
# otherwise it could have an incompatible signature when applying this overlay.
pkgs-locked = inputs.nixpkgs.legacyPackages.${pkgs.system};
neovim-nightly = inputs.neovim-nightly-overlay.packages.${pkgs.system}.default;
# This is the helper function that builds the Neovim derivation.
mkNeovim = pkgs.callPackage ./mkNeovim.nix {
inherit (pkgs-locked) wrapNeovimUnstable neovimUtils;
neovim-nightly = neovim-nightly;
};
callPackage = (file: pkgs.callPackage file {
@ -326,7 +324,11 @@ with final.pkgs.lib; let
in
{
codingPackages = extraPackages;
codingPackages = pkgs.buildEnv {
name = "coding-packages";
paths = extraPackages;
pathsToLink = [ "/bin" "/share" ];
};
# This is the neovim derivation
# returned by the overlay
nvim-pkg = mkNeovim {

View file

@ -1,104 +0,0 @@
local M = {
opts = {
allow_insecure = true,
show_defaults = false,
},
}
M.gemini = function()
return require("codecompanion.adapters").extend("gemini", {
env = {
api_key = "GEMINI_API_KEY",
},
})
end
M.airun = function()
return require("codecompanion.adapters").extend("openai_compatible", {
env = {
url = vim.env.AI_RUN_URL,
api_key = "AI_RUN_TOKEN", -- optional: if your endpoint is authenticated
chat_url = "/v1/chat/completions", -- optional: default value, override if different
models_endpoint = "/v1/models", -- optional: attaches to the end of the URL to form the endpoint to retrieve models
},
opts = {
vision = false,
tools = true,
stream = true,
},
schema = {
model = {
default = vim.env.AI_RUN_MODEL,
choices = function(self)
return {}
end,
},
num_ctx = {
default = 131072,
},
},
})
end
local function ollama_params(model_name, model)
return function()
return require("codecompanion.adapters").extend("ollama", {
env = {
url = vim.g.ollama_url,
},
headers = {
["Content-Type"] = "application/json",
},
parameters = {
sync = true,
keep_alive = "30m",
},
name = model_name,
schema = {
model = {
default = model,
},
num_ctx = {
default = 1024 * 32, -- 32768
},
temperature = {
default = 0.5,
},
},
})
end
end
local function ollama_modify(model_name, model, func)
local params_func = ollama_params(model_name, model)
return function()
local params = params_func()
return func(params)
end
end
M.ollama_deepseek = ollama_params("deepseek-r1", "deepseek-r1:14b-qwen-distill-q4_K_M")
M.ollama_deepcode = ollama_params("deepcode", "hf.co/lmstudio-community/DeepCoder-14B-Preview-GGUF:Q4_K_M")
M.ollama_gemma3 = ollama_params("gemma3", "hf.co/unsloth/gemma-3-12b-it-GGUF:Q4_K_M")
M.ollama_codegemma = ollama_params("codegemma", "codegemma:latest")
M.ollama_phimini = ollama_params("phimini", "phi4-mini:latest")
M.ollama_devstral = ollama_modify("devstral", "devstral:latest", function(params)
params.schema.temperature.default = 0.15
return params
end)
local function ollama_qwen(model_name, model)
return ollama_modify(model_name, model, function(params)
return params
end)
end
M.ollama_qwencoder = ollama_qwen("qwen2.5", "hf.co/unsloth/Qwen2.5-Coder-7B-Instruct-128K-GGUF:Q4_K_M")
M.ollama_qwen = ollama_qwen("qwen3", "hf.co/unsloth/Qwen3-8B-128K-GGUF:Q6_K_XL")
M.ollama_qwenamall = ollama_qwen("qwen3-small", "qwen3:0.6b")
M.ollama_qwenlarge = ollama_qwen("qwen3-large", "qwen3:30b-a3b")
M.default_adapter = M.airun
return M

View file

@ -1,140 +0,0 @@
return {
options = {
modes = {
n = "?",
},
callback = "keymaps.options",
description = "Options",
hide = true,
},
completion = {
modes = {
i = "<C-_>",
},
index = 1,
callback = "keymaps.completion",
description = "Completion Menu",
},
send = {
modes = {
n = { "<CR>", "<C-s>" },
i = "<C-s>",
},
index = 1,
callback = "keymaps.send",
description = "Send",
},
regenerate = {
modes = {
n = "gr",
},
index = 2,
callback = "keymaps.regenerate",
description = "Regenerate the last response",
},
close = {
modes = {
n = "<C-c>",
i = "<C-c>",
},
index = 3,
callback = "keymaps.stop",
description = "Stop Chat",
},
stop = {
modes = {
n = "q",
},
index = 4,
callback = "keymaps.stop",
description = "Stop Request",
},
clear = {
modes = {
n = "gx",
},
index = 5,
callback = "keymaps.clear",
description = "Clear Chat",
},
codeblock = {
modes = {
n = "gc",
},
index = 6,
callback = "keymaps.codeblock",
description = "Insert Codeblock",
},
yank_code = {
modes = {
n = "gy",
},
index = 7,
callback = "keymaps.yank_code",
description = "Yank Code",
},
next_chat = {
modes = {
n = "}",
},
index = 8,
callback = "keymaps.next_chat",
description = "Next Chat",
},
previous_chat = {
modes = {
n = "{",
},
index = 9,
callback = "keymaps.previous_chat",
description = "Previous Chat",
},
next_header = {
modes = {
n = "]]",
},
index = 10,
callback = "keymaps.next_header",
description = "Next Header",
},
previous_header = {
modes = {
n = "[[",
},
index = 11,
callback = "keymaps.previous_header",
description = "Previous Header",
},
change_adapter = {
modes = {
n = "ga",
},
index = 12,
callback = "keymaps.change_adapter",
description = "Change adapter",
},
fold_code = {
modes = {
n = "za",
},
index = 13,
callback = "keymaps.fold_code",
description = "Fold code",
},
debug = {
modes = {
n = "gd",
},
index = 14,
callback = "keymaps.debug",
description = "View debug info",
},
system_prompt = {
modes = {
n = "gs",
},
index = 15,
callback = "keymaps.toggle_system_prompt",
description = "Toggle the system prompt",
},
}

View file

@ -1,157 +0,0 @@
local util = require("codecompanion.utils")
local fmt = string.format
---@class CodeCompanion.Tool.CmdRunner: CodeCompanion.Tools.Tool
return {
name = "cmd_runner",
cmds = {
-- This is dynamically populated via the setup function
},
schema = {
type = "function",
["function"] = {
name = "cmd_runner",
description = "Run shell commands on the user's system, sharing the output with the user before then sharing with you.",
parameters = {
type = "object",
properties = {
cmd = {
type = "string",
description = "The command to run, e.g. `pytest` or `make test`",
},
flag = {
anyOf = {
{ type = "string" },
{ type = "null" },
},
description = 'If running tests, set to `"testing"`; null otherwise',
},
},
required = {
"cmd",
"flag",
},
additionalProperties = false,
},
},
},
system_prompt = fmt(
[[# Command Runner Tool (`cmd_runner`)
## CONTEXT
- You have access to a command runner tool running within CodeCompanion, in Neovim.
- You can use it to run shell commands on the user's system.
- You may be asked to run a specific command or to determine the appropriate command to fulfil the user's request.
- All tool executions take place in the current working directory %s.
## OBJECTIVE
- Follow the tool's schema.
- Respond with a single command, per tool execution.
## RESPONSE
- Only invoke this tool when the user specifically asks.
- If the user asks you to run a specific command, do so to the letter, paying great attention.
- Use this tool strictly for command execution; but file operations must NOT be executed in this tool unless the user explicitly approves.
- To run multiple commands, you will need to call this tool multiple times.
## SAFETY RESTRICTIONS
- Never execute the following dangerous commands under any circumstances:
- `rm -rf /` or any variant targeting root directories
- `rm -rf ~` or any command that could wipe out home directories
- `rm -rf .` without specific context and explicit user confirmation
- Any command with `:(){:|:&};:` or similar fork bombs
- Any command that would expose sensitive information (keys, tokens, passwords)
- Commands that intentionally create infinite loops
- For any destructive operation (delete, overwrite, etc.), always:
1. Warn the user about potential consequences
2. Request explicit confirmation before execution
3. Suggest safer alternatives when available
- If unsure about a command's safety, decline to run it and explain your concerns
## POINTS TO NOTE
- This tool can be used alongside other tools within CodeCompanion
## USER ENVIRONMENT
- Shell: %s
- Operating System: %s
- Neovim Version: %s]],
vim.fn.getcwd(),
vim.o.shell,
util.os(),
vim.version().major .. "." .. vim.version().minor .. "." .. vim.version().patch
),
handlers = {
---@param self CodeCompanion.Tool.CmdRunner
---@param tool CodeCompanion.Tools The tool object
setup = function(self, tool)
local args = self.args
local cmd = { cmd = vim.split(args.cmd, " ") }
if args.flag then
cmd.flag = args.flag
end
table.insert(self.cmds, cmd)
end,
},
output = {
---Prompt the user to approve the execution of the command
---@param self CodeCompanion.Tool.CmdRunner
---@param tool CodeCompanion.Tools
---@return string
prompt = function(self, tool)
return fmt("Run the command `%s`?", self.args.cmd)
end,
---Rejection message back to the LLM
---@param self CodeCompanion.Tool.CmdRunner
---@param tool CodeCompanion.Tools
---@param cmd table
---@return nil
rejected = function(self, tool, cmd)
tool.chat:add_tool_output(self, fmt("The user rejected the execution of the command `%s`?", self.args.cmd))
end,
---@param self CodeCompanion.Tool.CmdRunner
---@param tool CodeCompanion.Tools
---@param cmd table
---@param stderr table The error output from the command
error = function(self, tool, cmd, stderr)
local chat = tool.chat
local errors = vim.iter(stderr):flatten():join("\n")
local output = [[%s
```txt
%s
```]]
local llm_output = fmt(output, fmt("There was an error running the `%s` command:", cmd.cmd), errors)
local user_output = fmt(output, fmt("`%s` error", cmd.cmd), errors)
chat:add_tool_output(self, llm_output, user_output)
end,
---@param self CodeCompanion.Tool.CmdRunner
---@param tool CodeCompanion.Tools
---@param cmd table The command that was executed
---@param stdout table The output from the command
success = function(self, tool, cmd, stdout)
local chat = tool.chat
if stdout and vim.tbl_isempty(stdout) then
return chat:add_tool_output(self, "There was no output from the cmd_runner tool")
end
local output = vim.iter(stdout[#stdout]):flatten():join("\n")
local message = fmt(
[[`%s`
```
%s
```]],
self.args.cmd,
output
)
chat:add_tool_output(self, message)
end,
},
}

View file

@ -1,237 +0,0 @@
local files = require("codecompanion.utils.files")
local log = require("codecompanion.utils.log")
local fmt = string.format
---Create a file and the surrounding folders
---@param action {filepath: string, content: string} The action containing the filepath and content
---@return {status: "success"|"error", data: string}
local function create(action)
local filepath = vim.fs.joinpath(vim.fn.getcwd(), action.filepath)
filepath = vim.fs.normalize(filepath)
-- Check if file already exists
local stat = vim.uv.fs_stat(filepath)
if stat then
if stat.type == "directory" then
return {
status = "error",
data = fmt(
[[Failed creating `%s`
- Already exists as a directory]],
action.filepath
),
}
elseif stat.type == "file" then
return {
status = "error",
data = fmt(
[[Failed creating `%s`
- File already exists]],
action.filepath
),
}
end
end
local parent_dir = vim.fs.dirname(filepath)
-- Ensure parent directory exists
if not vim.uv.fs_stat(parent_dir) then
local success, err_msg = files.create_dir_recursive(parent_dir)
if not success then
local error_message = fmt(
[[Failed creating `%s`
- %s]],
action.filepath,
err_msg
)
log:error(error_message)
return { status = "error", data = error_message }
end
end
-- Create file with safer error handling
local fd, fs_open_err, fs_open_errname = vim.uv.fs_open(filepath, "w", 420) -- 0644 permissions
if not fd then
local error_message = fmt(
[[Failed creating `%s`
- %s
-%s ]],
action.filepath,
fs_open_err,
fs_open_errname
)
log:error(error_message)
return { status = "error", data = error_message }
end
-- Try to write to the file
local bytes_written, fs_write_err, fs_write_errname = vim.uv.fs_write(fd, action.content)
local write_error_message
if not bytes_written then
write_error_message = fmt(
[[Failed creating `%s`
- %s]],
action.filepath,
fs_write_err
)
elseif bytes_written ~= #action.content then
write_error_message = fmt(
[[Failed creating `%s`
- Could only write %s bytes]],
action.filepath,
bytes_written
)
end
-- Always try to close the file descriptor
local close_success, fs_close_err, fs_close_errname = vim.uv.fs_close(fd)
local close_error_message
if not close_success then
close_error_message = fmt(
[[Failed creating `%s`
- Could not close the file
- %s ]],
action.filepath,
fs_close_err
)
end
-- Combine errors if any
local final_error_message
if write_error_message and close_error_message then
final_error_message = write_error_message .. ". Additionally, " .. close_error_message
elseif write_error_message then
final_error_message = write_error_message
elseif close_error_message then
final_error_message = close_error_message
end
-- If any error occurred during write or close, return error
if final_error_message then
local full_error = fmt(
[[Failed creating `%s`
- %s]],
action.filepath,
final_error_message
)
log:error(full_error)
return { status = "error", data = full_error }
end
-- If we reach here, all operations (open, write, close) were successful
return {
status = "success",
data = fmt([[Created `%s`]], action.filepath),
}
end
---@class CodeCompanion.Tool.CreateFile: CodeCompanion.Tools.Tool
return {
name = "create_file",
cmds = {
---Execute the file commands
---@param self CodeCompanion.Tool.CreateFile
---@param args table The arguments from the LLM's tool call
---@param input? any The output from the previous function call
---@return { status: "success"|"error", data: string }
function(self, args, input)
return create(args)
end,
},
schema = {
type = "function",
["function"] = {
name = "create_file",
description = "This is a tool for creating a new file on the user's machine. The file will be created with the specified content, creating any necessary parent directories.",
parameters = {
type = "object",
properties = {
filepath = {
type = "string",
description = "The relative path to the file to create, including its filename and extension.",
},
content = {
type = "string",
description = "The content to write to the file.",
},
},
required = {
"filepath",
"content",
},
},
},
},
handlers = {
---@param tools CodeCompanion.Tools The tool object
---@return nil
on_exit = function(tools)
log:trace("[Create File Tool] on_exit handler executed")
end,
},
output = {
---The message which is shared with the user when asking for their approval
---@param self CodeCompanion.Tools.Tool
---@param tools CodeCompanion.Tools
---@return nil|string
prompt = function(self, tools)
local args = self.args
local filepath = vim.fn.fnamemodify(args.filepath, ":.")
return fmt("Create a file at %s?", filepath)
end,
---@param self CodeCompanion.Tool.CreateFile
---@param tools CodeCompanion.Tools
---@param cmd table The command that was executed
---@param stdout table The output from the command
success = function(self, tools, cmd, stdout)
local chat = tools.chat
local output = vim.iter(stdout):flatten():join("\n")
local args = self.args
local filepath = args.filepath
local llm_output = fmt("<createFileTool>%s</createFileTool>", "Created file `%s` successfully")
-- Get the file extension for syntax highlighting
local file_ext = vim.fn.fnamemodify(filepath, ":e")
local result_msg = fmt(
[[Created file `%s`
```%s
%s
```]],
filepath,
file_ext,
args.content or ""
)
chat:add_tool_output(self, llm_output, result_msg)
end,
---@param self CodeCompanion.Tool.CreateFile
---@param tools CodeCompanion.Tools
---@param cmd table
---@param stderr table The error output from the command
error = function(self, tools, cmd, stderr)
local chat = tools.chat
local args = self.args
local errors = vim.iter(stderr):flatten():join("\n")
log:debug("[Create File Tool] Error output: %s", stderr)
local error_output = fmt([[%s]], errors)
chat:add_tool_output(self, error_output)
end,
---Rejection message back to the LLM
---@param self CodeCompanion.Tool.CreateFile
---@param tools CodeCompanion.Tools
---@param cmd table
---@return nil
rejected = function(self, tools, cmd)
local chat = tools.chat
chat:add_tool_output(self, "User rejected the creation of the file")
end,
},
}

View file

@ -1,152 +0,0 @@
local adapters = require("codecompanion.adapters")
local client = require("codecompanion.http")
local config = require("codecompanion.config")
local log = require("codecompanion.utils.log")
local fmt = string.format
---@class CodeCompanion.Tool.FetchWebpage: CodeCompanion.Tools.Tool
return {
name = "fetch_webpage",
cmds = {
---Execute the fetch_webpage tool
---@param self CodeCompanion.Tools
---@param args table The arguments from the LLM's tool call
---@param cb function Async callback for completion
---@return nil
function(self, args, _, cb)
local opts = self.tool.opts
local url = args.url
if not opts or not opts.adapter then
log:error("[Fetch Webpage Tool] No adapter set for `fetch_webpage`")
return cb({ status = "error", data = "No adapter for `fetch_webpage`" })
end
if not args then
log:error("[Fetch Webpage Tool] No args for `fetch_webpage`")
return cb({ status = "error", data = "No args for `fetch_webpage`" })
end
if not url or type(url) ~= "string" or url == "" then
return cb({ status = "error", data = fmt("No URL for `fetch_webpage`") })
end
local tool_adapter = config.strategies.chat.tools.fetch_webpage.opts.adapter
local adapter = vim.deepcopy(adapters.resolve(tool_adapter))
adapter.methods.tools.fetch_webpage.setup(adapter, args)
if not url:match("^https?://") then
log:error("[Fetch Webpage Tool] Invalid URL: `%s`", url)
return cb({ status = "error", data = fmt("Invalid URL: `%s`", url) })
end
client
.new({
adapter = adapter,
})
:request(_, {
callback = function(err, data)
if err then
log:error("[Fetch Webpage Tool] Error fetching `%s`: %s", url, err)
return cb({ status = "error", data = fmt("Error fetching `%s`\n%s", url, err) })
end
if data then
local output = adapter.methods.tools.fetch_webpage.callback(adapter, data)
if output.status == "error" then
log:error("[Fetch Webpage Tool] Error processing data for `%s`: %s", url, output.content)
return cb({ status = "error", data = fmt("Error processing `%s`\n%s", url, output.content) })
end
return cb({ status = "success", data = output.content })
end
end,
})
end,
},
schema = {
type = "function",
["function"] = {
name = "fetch_webpage",
description = "Fetches the main content from a web page. You should use this tool when you think the user is looking for information from a specific webpage.",
parameters = {
type = "object",
properties = {
url = {
type = "string",
description = "The URL of the webpage to fetch content from",
},
},
required = { "url" },
},
},
},
output = {
---@param self CodeCompanion.Tool.FetchWebpage
---@param tools CodeCompanion.Tools
---@param cmd table The command that was executed
---@param stdout table The output from the command
success = function(self, tools, cmd, stdout)
local args = self.args
local chat = tools.chat
local content
if type(stdout) == "table" then
if #stdout == 1 and type(stdout[1]) == "string" then
content = stdout[1]
elseif #stdout == 1 and type(stdout[1]) == "table" then
-- If stdout[1] is a table, try to extract content
local first_item = stdout[1]
if type(first_item) == "table" and first_item.content then
content = first_item.content
else
-- Fallback: convert to string representation
content = vim.inspect(first_item)
end
else
-- Multiple items or other structure
content = vim
.iter(stdout)
:map(function(item)
if type(item) == "string" then
return item
elseif type(item) == "table" and item.content then
return item.content
else
return vim.inspect(item)
end
end)
:join("\n")
end
else
content = tostring(stdout)
end
local llm_output = fmt([[<attachment url="%s">%s</attachment>]], args.url, content)
local user_output = fmt("Fetched content from `%s`", args.url)
chat:add_tool_output(self, llm_output, user_output)
end,
---@param self CodeCompanion.Tool.FetchWebpage
---@param tools CodeCompanion.Tools
---@param cmd table
---@param stderr table The error output from the command
error = function(self, tools, cmd, stderr)
local args = self.args
local chat = tools.chat
local errors = vim.iter(stderr):flatten():join("\n")
log:debug("[Fetch Webpage Tool] Error output: %s", stderr)
local error_output = fmt(
[[Error fetching content from `%s`:
```txt
%s
```]],
args.url,
errors
)
chat:add_tool_output(self, error_output)
end,
},
}

View file

@ -1,183 +0,0 @@
local log = require("codecompanion.utils.log")
local fmt = string.format
---Search the current working directory for files matching the glob pattern.
---@param action { query: string, max_results: number }
---@param opts table
---@return { status: "success"|"error", data: string }
local function search(action, opts)
opts = opts or {}
local query = action.query
local max_results = action.max_results or opts.max_results or 500 -- Default limit to prevent overwhelming results
if not query or query == "" then
return {
status = "error",
data = "Query parameter is required and cannot be empty",
}
end
local cwd = vim.fn.getcwd()
-- Convert glob pattern to lpeg pattern for matching
local ok, glob_pattern = pcall(vim.glob.to_lpeg, query)
if not ok then
return {
status = "error",
data = fmt("Invalid glob pattern '%s': %s", query, glob_pattern),
}
end
-- Use vim.fs.find with a custom function that matches the glob pattern
local found_files = vim.fs.find(function(name, path)
local full_path = vim.fs.joinpath(path, name)
local relative_path = vim.fs.relpath(cwd, full_path)
if not relative_path then
return false
end
return glob_pattern:match(relative_path) ~= nil
end, {
limit = max_results,
type = "file",
path = cwd,
})
if #found_files == 0 then
return {
status = "success",
data = fmt("No files found matching pattern '%s'", query),
}
end
-- Convert absolute paths to relative paths so the LLM doesn't have full knowledge of the filesystem
local relative_files = {}
for _, file in ipairs(found_files) do
local rel_path = vim.fs.relpath(cwd, file)
if rel_path then
table.insert(relative_files, rel_path)
else
table.insert(relative_files, file)
end
end
return {
status = "success",
data = relative_files,
}
end
---@class CodeCompanion.Tool.FileSearch: CodeCompanion.Tools.Tool
return {
name = "file_search",
cmds = {
---Execute the search commands
---@param self CodeCompanion.Tool.FileSearch
---@param args table The arguments from the LLM's tool call
---@param input? any The output from the previous function call
---@return { status: "success"|"error", data: string }
function(self, args, input)
return search(args, self.tool.opts)
end,
},
schema = {
type = "function",
["function"] = {
name = "file_search",
description = "Search for files in the workspace by glob pattern. This only returns the paths of matching files. Use this tool when you know the exact filename pattern of the files you're searching for. Glob patterns match from the root of the workspace folder. Examples:\n- **/*.{js,ts} to match all js/ts files in the workspace.\n- src/** to match all files under the top-level src folder.\n- **/foo/**/*.js to match all js files under any foo folder in the workspace.",
parameters = {
type = "object",
properties = {
query = {
type = "string",
description = "Search for files with names or paths matching this glob pattern.",
},
max_results = {
type = "number",
description = "The maximum number of results to return. Do not use this unless necessary, it can slow things down. By default, only some matches are returned. If you use this and don't see what you're looking for, you can try again with a more specific query or a larger max_results.",
},
},
required = {
"query",
},
},
},
},
handlers = {
---@param tools CodeCompanion.Tools The tool object
---@return nil
on_exit = function(tools)
log:trace("[File Search Tool] on_exit handler executed")
end,
},
output = {
---The message which is shared with the user when asking for their approval
---@param self CodeCompanion.Tools.Tool
---@param tools CodeCompanion.Tools
---@return nil|string
prompt = function(self, tools)
local args = self.args
local query = args.query or ""
return fmt("Search the cwd for %s?", query)
end,
---@param self CodeCompanion.Tool.FileSearch
---@param tools CodeCompanion.Tools
---@param cmd table The command that was executed
---@param stdout table The output from the command
success = function(self, tools, cmd, stdout)
local chat = tools.chat
local query = self.args.query
local data = stdout[1]
local llm_output = "<fileSearchTool>%s</fileSearchTool>"
local output = vim.iter(stdout):flatten():join("\n")
if type(data) == "table" then
-- Files were found - data is an array of file paths
local files = #data
local results_msg = fmt("Searched files for `%s`, %d results\n```\n%s\n```", query, files, output)
chat:add_tool_output(self, fmt(llm_output, results_msg), results_msg)
else
-- No files found - data is a string message
local no_results_msg = fmt("Searched files for `%s`, no results", query)
chat:add_tool_output(self, fmt(llm_output, no_results_msg), no_results_msg)
end
end,
---@param self CodeCompanion.Tool.FileSearch
---@param tools CodeCompanion.Tools
---@param cmd table
---@param stderr table The error output from the command
error = function(self, tools, cmd, stderr)
local chat = tools.chat
local query = self.args.query
local errors = vim.iter(stderr):flatten():join("\n")
log:debug("[File Search Tool] Error output: %s", stderr)
local error_output = fmt(
[[Searched files for `%s`, error:
```txt
%s
```]],
query,
errors
)
chat:add_tool_output(self, error_output)
end,
---Rejection message back to the LLM
---@param self CodeCompanion.Tool.FileSearch
---@param tools CodeCompanion.Tools
---@param cmd table
---@return nil
rejected = function(self, tools, cmd)
local chat = tools.chat
chat:add_tool_output(self, "**File Search Tool**: The user declined to execute")
end,
},
}

View file

@ -1,169 +0,0 @@
local Job = require("plenary.job")
local log = require("codecompanion.utils.log")
local fmt = string.format
---@param state string
---@return string[]|nil, string|nil
local function get_git_diff(state, opts)
local cmd, desc
if state == "staged" then
cmd = { "git", "diff", "--cached" }
desc = "staged"
elseif state == "unstaged" then
cmd = { "git", "diff" }
desc = "unstaged"
elseif state == "merge-conflicts" then
cmd = { "git", "diff", "--name-only", "--diff-filter=U" }
desc = "merge-conflicts"
else
return nil, nil
end
local ok, result = pcall(function()
return Job:new({ command = cmd[1], args = vim.list_slice(cmd, 2), cwd = vim.fn.getcwd() }):sync()
end)
if not ok then
return nil, desc
end
if result and #result > opts.max_lines then
result = vim.list_slice(result, 1, opts.max_lines)
table.insert(result, "... (diff output truncated)")
end
return result, desc
end
---Get changed files in the current working directory based on the git state.
---@param action {source_control_state?: string[]}
---@param opts {max_lines: number}
---@return {status: "success"|"error", data: string}
local function get_changed_files(action, opts)
local states = action.source_control_state or { "unstaged", "staged", "merge-conflicts" }
local output = {}
for _, state in ipairs(states) do
local result, desc = get_git_diff(state, opts)
if desc then
if state == "merge-conflicts" then
if result and #result > 0 then
table.insert(
output,
fmt(
[[<getChangedFiles type="%s">
- %s
</getChangedFiles]],
desc,
table.concat(result, "\n- ")
)
)
end
else
if result and #result > 0 then
table.insert(
output,
fmt(
[[<getChangedFiles type="%s">
```diff
%s
```
</getChangedFiles>]],
desc,
table.concat(result, "\n")
)
)
end
end
end
end
if vim.tbl_isempty(output) then
return {
status = "success",
data = "No changed files found.",
}
end
return {
status = "success",
data = table.concat(output, "\n\n"),
}
end
---@class CodeCompanion.Tool.GetChangedFiles: CodeCompanion.Tools.Tool
return {
name = "get_changed_files",
cmds = {
---@param self CodeCompanion.Tool.GetChangedFiles
---@param args table
---@param input? any
function(self, args, input)
return get_changed_files(args, self.tool.opts)
end,
},
schema = {
type = "function",
["function"] = {
name = "get_changed_files",
description = "Get git diffs of current file changes in the current working directory.",
parameters = {
type = "object",
properties = {
source_control_state = {
type = "array",
items = {
type = "string",
enum = { "staged", "unstaged", "merge-conflicts" },
},
description = "The kinds of git state to filter by. Allowed values are: 'staged', 'unstaged', and 'merge-conflicts'. If not provided, all states will be included.",
},
},
},
},
},
handlers = {
---@param tools CodeCompanion.Tools The tool object
---@return nil
on_exit = function(tools)
log:trace("[Insert Edit Into File Tool] on_exit handler executed")
end,
},
output = {
---@param self CodeCompanion.Tool.GetChangedFiles
---@param tools CodeCompanion.Tools
prompt = function(self, tools)
return "Get changed files in the git repository?"
end,
---@param self CodeCompanion.Tool.GetChangedFiles
---@param tools CodeCompanion.Tools
---@param cmd table The command that was executed
---@param stdout table The output from the command
success = function(self, tools, cmd, stdout)
local chat = tools.chat
local output = vim.iter(stdout):flatten():join("\n")
chat:add_tool_output(self, output, "Reading changed files")
end,
---@param self CodeCompanion.Tool.GetChangedFiles
---@param tools CodeCompanion.Tools
---@param cmd table
---@param stderr table The error output from the command
error = function(self, tools, cmd, stderr)
local chat = tools.chat
local errors = vim.iter(stderr):flatten():join("\n")
chat:add_tool_output(self, errors)
end,
---Rejection message back to the LLM
---@param self CodeCompanion.Tool.GetChangedFiles
---@param tools CodeCompanion.Tools
---@param cmd table
---@return nil
rejected = function(self, tools, cmd)
local chat = tools.chat
chat:add_tool_output(self, "The user declined to get changed files")
end,
},
}

View file

@ -1,274 +0,0 @@
local log = require("codecompanion.utils.log")
local fmt = string.format
---Search the current working directory for text using ripgrep
---@param action { query: string, is_regexp: boolean?, include_pattern: string? }
---@param opts table
---@return { status: "success"|"error", data: string|table }
local function grep_search(action, opts)
opts = opts or {}
local query = action.query
if not query or query == "" then
return {
status = "error",
data = "Query parameter is required and cannot be empty",
}
end
-- Check if ripgrep is available
if vim.fn.executable("rg") ~= 1 then
return {
status = "error",
data = "ripgrep (rg) is not installed or not in PATH",
}
end
local cmd = { "rg" }
local cwd = vim.fn.getcwd()
local max_results = opts.max_results or 100
local is_regexp = action.is_regexp or false
local respect_gitignore = opts.respect_gitignore
if respect_gitignore == nil then
respect_gitignore = opts.respect_gitignore ~= false
end
-- Use JSON output for structured parsing
table.insert(cmd, "--json")
table.insert(cmd, "--line-number")
table.insert(cmd, "--no-heading")
table.insert(cmd, "--with-filename")
-- Regex vs fixed string
if not is_regexp then
table.insert(cmd, "--fixed-strings")
end
-- Case sensitivity
table.insert(cmd, "--ignore-case")
-- Gitignore handling
if not respect_gitignore then
table.insert(cmd, "--no-ignore")
end
-- File pattern filtering
if action.include_pattern and action.include_pattern ~= "" then
table.insert(cmd, "--glob")
table.insert(cmd, action.include_pattern)
end
-- Limit results per file - we'll limit total results in post-processing
table.insert(cmd, "--max-count")
table.insert(cmd, tostring(math.min(max_results, 50)))
-- Add the query
table.insert(cmd, query)
-- Add the search path
table.insert(cmd, cwd)
log:debug("[Grep Search Tool] Running command: %s", table.concat(cmd, " "))
-- Execute
local result = vim
.system(cmd, {
text = true,
timeout = 30000, -- 30 second timeout
})
:wait()
if result.code ~= 0 then
local error_msg = result.stderr or "Unknown error"
if result.code == 1 then
-- No matches found - this is not an error for ripgrep
return {
status = "success",
data = "No matches found for the query",
}
elseif result.code == 2 then
log:warn("[Grep Search Tool] Invalid arguments or regex: %s", error_msg)
return {
status = "error",
data = fmt("Invalid search pattern or arguments: %s", error_msg:match("^[^\n]*") or "Unknown error"),
}
else
log:error("[Grep Search Tool] Command failed with code %d: %s", result.code, error_msg)
return {
status = "error",
data = fmt("Search failed: %s", error_msg:match("^[^\n]*") or "Unknown error"),
}
end
end
local output = result.stdout or ""
if output == "" then
return {
status = "success",
data = "No matches found for the query",
}
end
-- Parse JSON output from ripgrep
local matches = {}
local count = 0
for line in output:gmatch("[^\n]+") do
if count >= max_results then
break
end
local ok, json_data = pcall(vim.json.decode, line)
if ok and json_data.type == "match" then
local file_path = json_data.data.path.text
local line_number = json_data.data.line_number
-- Convert absolute path to relative path from cwd
local relative_path = vim.fs.relpath(cwd, file_path) or file_path
-- Extract just the filename and directory
local filename = vim.fn.fnamemodify(relative_path, ":t")
local dir_path = vim.fn.fnamemodify(relative_path, ":h")
-- Format: "filename:line directory_path"
local match_entry = fmt("%s:%d %s", filename, line_number, dir_path == "." and "" or dir_path)
table.insert(matches, match_entry)
count = count + 1
end
end
if #matches == 0 then
return {
status = "success",
data = "No matches found for the query",
}
end
return {
status = "success",
data = matches,
}
end
---@class CodeCompanion.Tool.GrepSearch: CodeCompanion.Tools.Tool
return {
name = "grep_search",
cmds = {
---Execute the search commands
---@param self CodeCompanion.Tool.GrepSearch
---@param args table The arguments from the LLM's tool call
---@param input? any The output from the previous function call
---@return { status: "success"|"error", data: string|table }
function(self, args, input)
return grep_search(args, self.tool.opts)
end,
},
schema = {
["function"] = {
name = "grep_search",
description = "Do a text search in the workspace. Use this tool when you know the exact string you're searching for.",
parameters = {
type = "object",
properties = {
query = {
type = "string",
description = "The pattern to search for in files in the workspace. Can be a regex or plain text pattern",
},
is_regexp = {
type = "boolean",
description = "Whether the pattern is a regex. False by default.",
},
include_pattern = {
type = "string",
description = "Search files matching this glob pattern. Will be applied to the relative path of files within the workspace.",
},
},
required = {
"query",
},
},
},
type = "function",
},
handlers = {
---@param tools CodeCompanion.Tools The tool object
---@return nil
on_exit = function(tools)
log:trace("[Grep Search Tool] on_exit handler executed")
end,
},
output = {
---The message which is shared with the user when asking for their approval
---@param self CodeCompanion.Tools.Tool
---@param tools CodeCompanion.Tools
---@return nil|string
prompt = function(self, tools)
local args = self.args
local query = args.query or ""
return fmt("Perform a grep search for %s?", query)
end,
---@param self CodeCompanion.Tool.GrepSearch
---@param tools CodeCompanion.Tools
---@param cmd table The command that was executed
---@param stdout table The output from the command
success = function(self, tools, cmd, stdout)
local query = self.args.query
local chat = tools.chat
local data = stdout[1]
local llm_output = [[<grepSearchTool>%s
NOTE:
- The output format is {filename}:{line number} {filepath}.
- For example:
init.lua:335 lua/codecompanion/strategies/chat/tools
Refers to line 335 of the init.lua file in the lua/codecompanion/strategies/chat/tools path</grepSearchTool>]]
local output = vim.iter(stdout):flatten():join("\n")
if type(data) == "table" then
-- Results were found - data is an array of file paths
local results = #data
local results_msg = fmt("Searched text for `%s`, %d results\n```\n%s\n```", query, results, output)
chat:add_tool_output(self, fmt(llm_output, results_msg), results_msg)
else
-- No results found - data is a string message
local no_results_msg = fmt("Searched text for `%s`, no results", query)
chat:add_tool_output(self, fmt(llm_output, no_results_msg), no_results_msg)
end
end,
---@param self CodeCompanion.Tool.GrepSearch
---@param tools CodeCompanion.Tools
---@param cmd table
---@param stderr table The error output from the command
error = function(self, tools, cmd, stderr)
local chat = tools.chat
local query = self.args.query
local errors = vim.iter(stderr):flatten():join("\n")
log:debug("[Grep Search Tool] Error output: %s", stderr)
local error_output = fmt(
[[Searched text for `%s`, error:
```
%s
```]],
query,
errors
)
chat:add_tool_output(self, error_output)
end,
---Rejection message back to the LLM
---@param self CodeCompanion.Tool.GrepSearch
---@param tools CodeCompanion.Tools
---@param cmd table
---@return nil
rejected = function(self, tools, cmd)
local chat = tools.chat
chat:add_tool_output(self, "**Grep Search Tool**: The user declined to execute")
end,
},
}

View file

@ -1,420 +0,0 @@
local log = require("codecompanion.utils.log")
---@class CodeCompanion.Patch
---@field prompt string The prompt text explaining the patch format to the LLM
---@field parse_edits fun(raw: string): CodeCompanion.Patch.Edit[], boolean, nil|string Parse raw LLM output into changes and whether markers were found
---@field apply fun(lines: string[], edit: CodeCompanion.Patch.Edit): string[]|nil,string|nil Apply an edit to file lines, returns nil if can't be confidently applied
---@field start_line fun(lines: string[], edit: CodeCompanion.Patch.Edit): number|nil Get the line number (1-based) where edit would be applied
---@field format fun(edit: CodeCompanion.Patch.Edit): string Format an edit object as a readable string for display/logging
local Patch = {}
Patch.prompt = [[ Use this exact format for all file edits:
*** Begin Patch
[PATCH]
*** End Patch
The `[PATCH]` is the series of diffs to be applied for each edit in the file. Each diff should be in this format:
*** Begin Patch
[3 lines of context before]
-[lines to remove]
+[lines to add]
[3 lines of context after]
*** End Patch
The context blocks are 3 lines of existing code, immediately before and after the modified lines of code.
Lines to be modified should be prefixed with a `+` or `-` sign.
Unmodified lines used in context should begin with an empty space ` `.
For example, to add a subtract method to a calculator class in Python:
*** Begin Patch
def add(self, value):
self.result += value
return self.result
+def subtract(self, value):
+ self.result -= value
+ return self.result
+
def multiply(self, value):
self.result *= value
return self.result
*** End Patch
Multiple blocks of diffs should be separated by an empty line and `@@[identifier]` as detailed below.
The immediately preceding and after context lines are enough to locate the lines to edit. DO NOT USE line numbers anywhere in the patch.
You can use `@@[identifier]` to define a larger context in case the immediately before and after context is not sufficient to locate the edits. Example:
@@class BaseClass(models.Model):
[3 lines of pre-context]
- pass
+ raise NotImplementedError()
[3 lines of post-context]
You can also use multiple `@@[identifiers]` to provide the right context if a single `@@` is not sufficient.
Example with multiple blocks of edits and `@@` identifiers:
*** Begin Patch
@@class BaseClass(models.Model):
@@ def search():
- pass
+ raise NotImplementedError()
@@class Subclass(BaseClass):
@@ def search():
- pass
+ raise NotImplementedError()
*** End Patch
This format is similar to the `git diff` format; the difference is that `@@[identifiers]` uses the unique line identifiers from the preceding code instead of line numbers. We don't use line numbers anywhere since the before and after context, and `@@` identifiers are enough to locate the edits.
IMPORTANT: Be mindful that the user may have shared attachments that contain line numbers, but these should NEVER be used in your patch. Always use the contextual format described above.]]
---@class CodeCompanion.Patch.Edit
---@field focus string[] Identifiers or lines for providing large context before an edit
---@field pre string[] Unchanged lines immediately before edits
---@field old string[] Lines to be removed
---@field new string[] Lines to be added
---@field post string[] Unchanged lines just after edits
---Create and return a new (empty) edit
---@param focus? string[] Optional focus lines for context
---@param pre? string[] Optional pre-context lines
---@return CodeCompanion.Patch.Edit New edit object
local function get_new_edit(focus, pre)
return {
focus = focus or {},
pre = pre or {},
old = {},
new = {},
post = {},
}
end
---Parse a patch string into a list of edits
---@param patch string Patch containing the edits
---@return CodeCompanion.Patch.Edit[] List of parsed edit blocks
local function parse_edits_from_patch(patch)
local edits = {}
local edit = get_new_edit()
local lines = vim.split(patch, "\n", { plain = true })
for i, line in ipairs(lines) do
if vim.startswith(line, "@@") then
if #edit.old > 0 or #edit.new > 0 then
-- @@ after any edits is a new edit block
table.insert(edits, edit)
edit = get_new_edit()
end
-- focus name can be empty too to signify new blocks
local focus_name = vim.trim(line:sub(3))
if focus_name and #focus_name > 0 then
edit.focus[#edit.focus + 1] = focus_name
end
elseif line == "" and lines[i + 1] and lines[i + 1]:match("^@@") then
-- empty lines can be part of pre/post context
-- we treat empty lines as a new edit block and not as post context
-- only when the next line uses @@ identifier
-- skip this line and do nothing
do
end
elseif line:sub(1, 1) == "-" then
if #edit.post > 0 then
-- edits after post edit lines are new block of changes with same focus
table.insert(edits, edit)
edit = get_new_edit(edit.focus, edit.post)
end
edit.old[#edit.old + 1] = line:sub(2)
elseif line:sub(1, 1) == "+" then
if #edit.post > 0 then
-- edits after post edit lines are new block of changes with same focus
table.insert(edits, edit)
edit = get_new_edit(edit.focus, edit.post)
end
edit.new[#edit.new + 1] = line:sub(2)
elseif #edit.old == 0 and #edit.new == 0 then
edit.pre[#edit.pre + 1] = line
elseif #edit.old > 0 or #edit.new > 0 then
edit.post[#edit.post + 1] = line
end
end
table.insert(edits, edit)
return edits
end
---Parse the edits from the LLM for all patches, returning all parsed edits
---@param raw string Raw text containing patch blocks
---@return CodeCompanion.Patch.Edit, boolean, string|nil All parsed edits, and whether the patch was properly parsed
function Patch.parse_edits(raw)
local patches = {}
for patch in raw:gmatch("%*%*%* Begin Patch[\r\n]+(.-)[\r\n]+%*%*%* End Patch") do
table.insert(patches, patch)
end
local had_begin_end_markers = true
local parse_error = nil
if #patches == 0 then
--- LLMs miss the begin / end markers sometimes
--- let's assume the raw content was correctly wrapped in these cases
--- setting a `markers_error` so that we can show this error in case the patch fails to apply
had_begin_end_markers = false
table.insert(patches, raw)
parse_error = "Missing Begin/End patch markers - assuming entire content is a patch"
end
local all_edits = {}
for _, patch in ipairs(patches) do
local edits = parse_edits_from_patch(patch)
for _, edit in ipairs(edits) do
table.insert(all_edits, edit)
end
end
return all_edits, had_begin_end_markers, parse_error
end
---Score how many lines from needle match haystack lines
---@param haystack string[] All file lines
---@param pos number Starting index to check (1-based)
---@param needle string[] Lines to match
---@return number Score: 10 per perfect line, or 9 per trimmed match
local function get_score(haystack, pos, needle)
local score = 0
for i, needle_line in ipairs(needle) do
local hayline = haystack[pos + i - 1]
if hayline == needle_line then
score = score + 10
elseif hayline and vim.trim(hayline) == vim.trim(needle_line) then
score = score + 9
end
end
return score
end
---Compute the match score for focus lines above a position.
---@param lines string[] Lines of source file
---@param before_pos number Scan up to this line (exclusive; 1-based)
---@param focus string[] Focus lines/context
---@return number Score: 20 per matching focus line before position
local function get_focus_score(lines, before_pos, focus)
local start = 1
local score = 0
for _, focus_line in ipairs(focus) do
for k = start, before_pos - 1 do
if focus_line == lines[k] or (vim.trim(focus_line) == vim.trim(lines[k])) then
score = score + 20
start = k
break
end
end
end
return score
end
---Get the overall score for placing an edit on a given line
---@param lines string[] File lines
---@param edit CodeCompanion.Patch.Edit To match
---@param i number Line position
---@return number Score from 0.0 to 1.0
local function get_match_score(lines, edit, i)
local max_score = (#edit.focus * 2 + #edit.pre + #edit.old + #edit.post) * 10
local score = get_focus_score(lines, i, edit.focus)
+ get_score(lines, i - #edit.pre, edit.pre)
+ get_score(lines, i, edit.old)
+ get_score(lines, i + #edit.old, edit.post)
return score / max_score
end
---Determine best insertion spot for an edit and its match score
---@param lines string[] File lines
---@param edit CodeCompanion.Patch.Edit Patch block
---@return number, number location (1-based), Score (0-1)
local function get_best_location(lines, edit)
-- try applying patch in flexible spaces mode
-- there is no standardised way to of spaces in diffs
-- python differ specifies a single space after +/-
-- while gnu udiff uses no spaces
--
-- and LLM models (especially Claude) sometimes strip
-- long spaces on the left in case of large nestings (eg html)
-- trim_spaces mode solves all of these
local best_location = 1
local best_score = 0
for i = 1, #lines + 1 do
local score = get_match_score(lines, edit, i)
if score == 1 then
return i, 1
end
if score > best_score then
best_location = i
best_score = score
end
end
return best_location, best_score
end
---Get the start line location where an edit would be applied without actually applying it
---@param lines string[] File lines
---@param edit CodeCompanion.Patch.Edit Edit description
---@return number|nil location The line number (1-based) where the edit would be applied
function Patch.start_line(lines, edit)
local location, score = get_best_location(lines, edit)
if score < 0.5 then
return nil
end
return location
end
---Check if an edit is a simple append operation for small/empty files
---@param lines string[] Current file lines
---@param edit CodeCompanion.Patch.Edit The edit to analyze
---@return boolean is_simple_append
---@return string[]? lines_to_append
local function is_simple_append(lines, edit)
-- For empty files (containing only "")
if #lines == 1 and lines[1] == "" then
log:debug("[Patch] Empty file detected, treating as simple append")
return true, edit.new
end
-- For small files with simple append patterns
if #lines <= 5 and #edit.old == 0 and #edit.new > 0 then
-- Check if pre-context matches the end of the file or is empty
if #edit.pre == 0 then
log:debug("[Patch] No pre-context, appending to end of small file")
return true, edit.new
end
-- Check if pre-context matches the last lines of the file
local matches = true
local start_check = math.max(1, #lines - #edit.pre + 1)
for i, pre_line in ipairs(edit.pre) do
local file_line = lines[start_check + i - 1]
if not file_line or (vim.trim(file_line) ~= vim.trim(pre_line)) then
matches = false
break
end
end
if matches then
log:debug("[Patch] Pre-context matches, treating as simple append")
return true, edit.new
end
end
return false, nil
end
---Apply an edit to the file lines. Returns nil if not confident
---@param lines string[] Lines before patch
---@param edit CodeCompanion.Patch.Edit Edit description
---@return string[]|nil,string|nil New file lines (or nil if patch can't be confidently placed)
function Patch.apply(lines, edit)
-- Handle small files and empty files with special logic
if #lines <= 5 then
local is_append, append_lines = is_simple_append(lines, edit)
if is_append and append_lines then
log:debug("[Patch] Using simple append for small file")
local new_lines = {}
-- For empty files, don't include the empty string
if #lines == 1 and lines[1] == "" then
for _, line in ipairs(append_lines) do
table.insert(new_lines, line)
end
else
-- Copy existing lines and append new ones
for _, line in ipairs(lines) do
table.insert(new_lines, line)
end
for _, line in ipairs(append_lines) do
table.insert(new_lines, line)
end
end
log:debug("[Patch] Small file append successful, new line count: %d", #new_lines)
return new_lines
end
end
local location, score = get_best_location(lines, edit)
if score < 0.5 then
local error_msg = string.format(
"Could not confidently apply edit (confidence: %.1f%%). %s",
score * 100,
score < 0.2 and "The context doesn't match the file content."
or "Try providing more specific context or checking for formatting differences."
)
log:debug("[Patch] Low confidence score (%.2f), edit details: %s", score, Patch.format(edit))
return nil, error_msg
end
local new_lines = {}
-- add lines before diff
for k = 1, location - 1 do
new_lines[#new_lines + 1] = lines[k]
end
-- add new lines
local fix_spaces
-- infer adjustment of spaces from the delete line
if score ~= 1 and #edit.old > 0 then
if edit.old[1] == " " .. lines[location] then
-- diff patch added and extra space on left
fix_spaces = function(ln)
return ln:sub(2)
end
elseif #edit.old[1] < #lines[location] then
-- diff removed spaces on left
local prefix = string.rep(" ", #lines[location] - #edit.old[1])
fix_spaces = function(ln)
return prefix .. ln
end
end
end
for _, ln in ipairs(edit.new) do
if fix_spaces then
ln = fix_spaces(ln)
end
new_lines[#new_lines + 1] = ln
end
-- add remaining lines
for k = location + #edit.old, #lines do
new_lines[#new_lines + 1] = lines[k]
end
return new_lines, nil
end
---Join a list of lines, prefixing each optionally
---@param list string[] List of lines
---@param sep string Separator (e.g., "\n")
---@param prefix? string Optional prefix for each line
---@return string|false Result string or false if list is empty
local function prefix_join(list, sep, prefix)
if #list == 0 then
return false
end
if prefix then
for i = 1, #list do
list[i] = prefix .. list[i]
end
end
return table.concat(list, sep)
end
---Format an edit block as a string for output or logs
---@param edit CodeCompanion.Patch.Edit To render
---@return string Formatted string
function Patch.format(edit)
local parts = {
prefix_join(edit.focus, "\n", "@@"),
prefix_join(edit.pre, "\n"),
prefix_join(edit.old, "\n", "-"),
prefix_join(edit.new, "\n", "+"),
prefix_join(edit.post, "\n"),
}
local non_empty = {}
for _, part in ipairs(parts) do
if part then
table.insert(non_empty, part)
end
end
return table.concat(non_empty, "\n")
end
return Patch

View file

@ -1,230 +0,0 @@
return {
calculator = {
description = "Perform calculations",
opts = {
requires_approval = true,
},
callback = {
name = "calculator",
cmds = {
---@param self CodeCompanion.Tool.Calculator The Calculator tool
---@param args table The arguments from the LLM's tool call
---@param input? any The output from the previous function call
---@return nil|{ status: "success"|"error", data: string }
function(self, args, input)
-- Get the numbers and operation requested by the LLM
local num1 = tonumber(args.num1)
local num2 = tonumber(args.num2)
local operation = args.operation
-- Validate input
if not num1 then
return { status = "error", data = "First number is missing or invalid" }
end
if not num2 then
return { status = "error", data = "Second number is missing or invalid" }
end
if not operation then
return { status = "error", data = "Operation is missing" }
end
-- Perform the calculation
local result
if operation == "add" then
result = num1 + num2
elseif operation == "subtract" then
result = num1 - num2
elseif operation == "multiply" then
result = num1 * num2
elseif operation == "divide" then
if num2 == 0 then
return { status = "error", data = "Cannot divide by zero" }
end
result = num1 / num2
else
return {
status = "error",
data = "Invalid operation: must be add, subtract, multiply, or divide",
}
end
return { status = "success", data = result }
end,
},
system_prompt = [[## Calculator Tool (`calculator`)
## CONTEXT
- You have access to a calculator tool running within CodeCompanion, in Neovim.
- You can use it to add, subtract, multiply or divide two numbers.
### OBJECTIVE
- Do a mathematical operation on two numbers when the user asks
### RESPONSE
- Always use the structure above for consistency.
]],
schema = {
type = "function",
["function"] = {
name = "calculator",
description = "Perform simple mathematical operations on a user's machine",
parameters = {
type = "object",
properties = {
num1 = {
type = "integer",
description = "The first number in the calculation",
},
num2 = {
type = "integer",
description = "The second number in the calculation",
},
operation = {
type = "string",
enum = { "add", "subtract", "multiply", "divide" },
description = "The mathematical operation to perform on the two numbers",
},
},
required = {
"num1",
"num2",
"operation",
},
additionalProperties = false,
},
},
},
handlers = {
---@param self CodeCompanion.Tool.Calculator
---@param tools CodeCompanion.Tools The tool object
setup = function(self, tools)
return vim.notify("setup function called", vim.log.levels.INFO)
end,
---@param self CodeCompanion.Tool.Calculator
---@param tools CodeCompanion.Tools
on_exit = function(self, tools)
return vim.notify("on_exit function called", vim.log.levels.INFO)
end,
},
output = {
---@param self CodeCompanion.Tool.Calculator
---@param tools CodeCompanion.Tools
---@param cmd table The command that was executed
---@param stdout table
success = function(self, tools, cmd, stdout)
local chat = tools.chat
return chat:add_tool_output(self, tostring(stdout[1]))
end,
---@param self CodeCompanion.Tool.Calculator
---@param tools CodeCompanion.Tools
---@param cmd table
---@param stderr table The error output from the command
error = function(self, tools, cmd, stderr)
return vim.notify("An error occurred", vim.log.levels.ERROR)
end,
},
},
},
["cmd_runner"] = {
callback = "ai.tools.cmd_runner",
description = "Run shell commands initiated by the LLM",
},
["next_edit_suggestion"] = {
callback = "ai.tools.next_edit_suggestion",
description = "Suggest and jump to the next position to edit",
},
["insert_edit_into_file"] = {
callback = "ai.tools.insert_edit_into_file",
description = "Insert code into an existing file",
opts = {
patching_algorithm = "ai.tools.helpers.patch",
},
},
["create_file"] = {
callback = "ai.tools.create_file",
description = "Create a file in the current working directory",
},
["fetch_webpage"] = {
callback = "ai.tools.fetch_webpage",
description = "Fetches content from a webpage",
opts = {
adapter = "jina",
},
},
["search_web"] = {
callback = "ai.tools.search_web",
description = "Searches the web for a given query",
opts = {
adapter = "tavily",
},
},
["file_search"] = {
callback = "ai.tools.file_search",
description = "Search for files in the current working directory by glob pattern",
opts = {
max_results = 500,
},
},
["grep_search"] = {
callback = "ai.tools.grep_search",
description = "Search for text in the current working directory",
},
["read_file"] = {
callback = "ai.tools.read_file",
description = "Read a file in the current working directory",
},
["list_code_usages"] = {
callback = "ai.tools.list_code_usages",
description = "Find code symbol context",
},
groups = {
["senior_dev"] = {
description = "Tool Group",
prompt = "I'm giving you access to ${tools} to help me out",
tools = {
"func",
"cmd",
},
},
["tool_group"] = {
description = "Tool Group",
system_prompt = "My tool group system prompt",
tools = {
"func",
"cmd",
},
},
["remove_group"] = {
description = "Group to be removed during testing of context",
system_prompt = "System prompt to be removed",
tools = { "func", "weather" },
opts = { collapse_tools = true },
},
},
opts = {
requires_approval = true,
-- default_tools = {
-- "cmd_runner",
-- "grep_search",
-- "read_file",
-- "insert_edit_into_file",
-- "create_file",
-- "file_search",
-- "next_edit_suggestion",
-- },
-- auto_submit_errors = true,
-- auto_submit_success = true,
folds = {
enabled = true,
failure_words = {
"error",
"failed",
"invalid",
},
},
},
}

View file

@ -1,483 +0,0 @@
local Path = require("plenary.path")
local codecompanion = require("codecompanion")
local config = require("codecompanion.config")
local diff = require("codecompanion.strategies.chat.helpers.diff")
local helpers = require("codecompanion.strategies.chat.helpers")
local patch = require("codecompanion.strategies.chat.tools.catalog.helpers.patch") ---@type CodeCompanion.Patch
local wait = require("codecompanion.strategies.chat.helpers.wait")
local buffers = require("codecompanion.utils.buffers")
local log = require("codecompanion.utils.log")
local ui = require("codecompanion.utils.ui")
local api = vim.api
local fmt = string.format
local PROMPT = [[<editFileInstructions>
CRITICAL: ALL patches MUST be wrapped in *** Begin Patch / *** End Patch markers!
Before editing a file, ensure you have its content via the provided context or read_file tool.
Use the insert_edit_into_file tool to modify files.
NEVER show the code edits to the user - only call the tool. The system will apply and display the edits.
For each file, give a short description of what needs to be edited, then use the insert_edit_into_file tools. You can use the tool multiple times in a response, and you can keep writing text after using a tool.
The insert_edit_into_file tool is very smart and can understand how to apply your edits to the user's files, you just need to follow the patch format instructions carefully and to the letter.
## Patch Format
]] .. patch.prompt .. [[
The system uses fuzzy matching and confidence scoring so focus on providing enough context to uniquely identify the location.
REMEMBER: No *** Begin Patch / *** End Patch markers = FAILED EDIT!
</editFileInstructions>]]
---Resolve the patching algorithm module used to apply the edits to a file
---@param algorithm string|table|function The patch configuration, can be a module path, a table, or a function that returns a table
---@return CodeCompanion.Patch The resolved patch module
local function resolve_patch_module(algorithm)
if type(algorithm) == "table" then
return algorithm --[[@as CodeCompanion.Patch]]
end
if type(algorithm) == "function" then
return algorithm() --[[@as CodeCompanion.Patch]]
end
-- Try as a local module
local ok, module = pcall(require, "codecompanion." .. algorithm)
if ok then
return module --[[@as CodeCompanion.Patch]]
end
-- Try as file path
local file_module, _ = loadfile(algorithm)
if file_module then
return file_module() --[[@as CodeCompanion.Patch]]
end
error(string.format("Could not resolve the patch algorithm module: %s", algorithm))
end
---Edit code in a file
---@param action {filepath: string, code: string, explanation: string} The arguments from the LLM's tool call
---@param chat CodeCompanion.Chat The chat instance
---@param output_handler function The callback to call when done
---@param opts? table Additional options
---@return nil
local function edit_file(action, chat, output_handler, opts)
opts = opts or {}
local filepath = helpers.validate_and_normalize_filepath(action.filepath)
if not filepath then
return output_handler({
status = "error",
data = fmt("Error: Invalid or non-existent filepath `%s`", action.filepath),
})
end
local p = Path:new(filepath)
if not p:exists() or not p:is_file() then
return output_handler({
status = "error",
data = fmt("Error editing `%s`\nFile does not exist or is not a file", filepath),
})
end
-- 1. extract list of edits from the code
local raw = action.code or ""
local edits, had_begin_end_markers, parse_error = patch.parse_edits(raw)
-- 2. read file into lines
local content = p:read()
local lines = vim.split(content, "\n", { plain = true })
local original_content = vim.deepcopy(lines)
-- 3. apply edits
local all_errors = {}
if parse_error then
table.insert(all_errors, parse_error)
end
for i, edit in ipairs(edits) do
local new_lines, error_msg = patch.apply(lines, edit)
if error_msg then
table.insert(all_errors, fmt("Edit %d: %s", i, error_msg))
if not had_begin_end_markers then
table.insert(all_errors, "Hint: Try wrapping your patch in *** Begin Patch / *** End Patch markers")
end
elseif new_lines then
lines = new_lines
else
table.insert(all_errors, fmt("Edit %d: Unknown error applying patch", i))
end
end
-- Return errors
if #all_errors > 0 then
return output_handler({
status = "error",
data = table.concat(all_errors, "\n"),
})
end
-- 4. write back
p:write(table.concat(lines, "\n"), "w")
-- 5. refresh and format the buffer if the file is open
local bufnr = vim.fn.bufnr(p.filename)
if bufnr ~= -1 and api.nvim_buf_is_loaded(bufnr) then
pcall(api.nvim_command, "checktime " .. bufnr)
end
-- Auto-save if enabled
if vim.g.codecompanion_yolo_mode then
log:info("[Insert Edit Into File Tool] Auto-mode enabled, skipping diff and approval")
return output_handler({
status = "success",
data = fmt("Edited `%s`\n%s", action.filepath, action.explanation),
})
end
-- 6. Create diff for the file using new file path capability
helpers.hide_chat_for_floating_diff(chat)
local diff_id = math.random(10000000)
local should_diff = diff.create(p.filename, diff_id, {
original_content = original_content,
})
if should_diff then
log:debug("[Insert Edit Into File Tool] Diff created for file: %s", p.filename)
end
local success = {
status = "success",
data = fmt("Edited `%s`\n%s", action.filepath, action.explanation),
}
if should_diff and opts.user_confirmation then
log:debug("[Insert Edit Into File Tool] Setting up diff approval workflow for file")
local accept = config.strategies.inline.keymaps.accept_change.modes.n
local reject = config.strategies.inline.keymaps.reject_change.modes.n
local wait_opts = {
chat_bufnr = chat.bufnr,
notify = config.display.icons.warning .. " Waiting for decision ...",
sub_text = fmt("`%s` - Accept edits / `%s` - Reject edits", accept, reject),
}
-- Wait for the user to accept or reject the edit
return wait.for_decision(diff_id, { "CodeCompanionDiffAccepted", "CodeCompanionDiffRejected" }, function(result)
local response
if result.accepted then
log:debug("[Insert Edit Into File Tool] User accepted file changes")
response = success
else
log:debug("[Insert Edit Into File Tool] User rejected file changes")
-- Clean up diff on timeout
if result.timeout and should_diff and should_diff.reject then
should_diff:reject() -- Only clean up on timeout
end
response = {
status = "error",
data = result.timeout and "User failed to accept the edits in time" or "User rejected the edits",
}
end
-- NOTE: This is required to ensure folding works for chat buffers that aren't visible
codecompanion.restore(chat.bufnr)
return output_handler(response)
end, wait_opts)
else
log:debug("[Insert Edit Into File Tool] No user confirmation needed for file, returning success")
return output_handler(success)
end
end
---Edit code in a buffer
---@param bufnr number The buffer number to edit
---@param chat CodeCompanion.Chat The chat instance
---@param action {filepath: string, code: string, explanation: string} The arguments from the LLM's tool call
---@param output_handler function The callback to call when done
---@param opts? table Additional options
---@return string|nil, string|nil
local function edit_buffer(bufnr, chat, action, output_handler, opts)
opts = opts or {}
local should_diff
local diff_id = math.random(10000000)
local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false)
local original_content = vim.deepcopy(lines)
-- Parse and apply patches to buffer
local raw = action.code or ""
local edits, had_begin_end_markers, parse_error = patch.parse_edits(raw)
-- Apply each edit
local start_line = nil
local all_errors = {}
if parse_error then
table.insert(all_errors, parse_error)
end
for i, edit in ipairs(edits) do
log:debug("[Insert Edit Into File Tool] Applying buffer edit %d/%d", i, #edits)
local new_lines, error_msg = patch.apply(lines, edit)
if error_msg then
table.insert(all_errors, fmt("Edit %d: %s", i, error_msg))
if not had_begin_end_markers then
table.insert(all_errors, "Hint: Try wrapping your patch in *** Begin Patch / *** End Patch markers")
end
elseif new_lines then
if not start_line then
start_line = patch.start_line(lines, edit)
log:debug("[Insert Edit Into File Tool] Buffer edit start line: %d", start_line or -1)
end
log:debug("[Insert Edit Into File Tool] Buffer edit %d successful, new line count: %d", i, #new_lines)
lines = new_lines
else
table.insert(all_errors, fmt("Edit %d: Unknown error applying patch", i))
end
end
if #all_errors > 0 then
local error_output = table.concat(all_errors, "\n")
return output_handler({
status = "error",
data = error_output,
})
end
log:debug("[Insert Edit Into File Tool] All buffer edits applied successfully, final line count: %d", #lines)
-- Update the buffer with the edited code
api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
log:debug("[Insert Edit Into File Tool] Buffer content updated")
-- Create diff with original content
if original_content then
helpers.hide_chat_for_floating_diff(chat)
log:debug("[Insert Edit Into File Tool] Creating diff with original content (%d lines)", #original_content)
should_diff = diff.create(bufnr, diff_id, {
original_content = original_content,
})
if should_diff then
log:debug("[Insert Edit Into File Tool] Diff created successfully with ID: %s", diff_id)
else
log:debug("[Insert Edit Into File Tool] Diff creation returned nil")
end
else
log:debug("[Insert Edit Into File Tool] No original content captured, skipping diff creation")
end
-- Scroll to the editing location
if start_line then
log:debug("[Insert Edit Into File Tool] Scrolling to line %d", start_line)
ui.scroll_to_line(bufnr, start_line)
end
-- Auto-save if enabled
if vim.g.codecompanion_yolo_mode then
log:info("[Insert Edit Into File Tool] Auto-saving buffer %d", bufnr)
api.nvim_buf_call(bufnr, function()
vim.cmd("silent write")
end)
end
local success = {
status = "success",
data = fmt("Edited `%s`\n%s", action.filepath, action.explanation),
}
if should_diff and opts.user_confirmation then
log:debug("[Insert Edit Into File Tool] Setting up diff approval workflow")
local accept = config.strategies.inline.keymaps.accept_change.modes.n
local reject = config.strategies.inline.keymaps.reject_change.modes.n
local wait_opts = {
chat_bufnr = chat.bufnr,
notify = config.display.icons.warning .. " Waiting for diff approval ...",
sub_text = fmt("`%s` - Accept edits / `%s` - Reject edits", accept, reject),
}
-- Wait for the user to accept or reject the edit
return wait.for_decision(diff_id, { "CodeCompanionDiffAccepted", "CodeCompanionDiffRejected" }, function(result)
local response
if result.accepted then
log:debug("[Insert Edit Into File Tool] User accepted changes")
-- Save the buffer
pcall(function()
api.nvim_buf_call(bufnr, function()
vim.cmd("silent! w")
end)
end)
response = success
else
log:debug("[Insert Edit Into File Tool] User rejected changes")
-- Clean up diff on timeout
if result.timeout and should_diff and should_diff.reject then
should_diff:reject() -- Only clean up on timeout
end
response = {
status = "error",
data = result.timeout and "User failed to accept the edits in time" or "User rejected the edits",
}
end
-- NOTE: This is required to ensure folding works for chat buffers that aren't visible
codecompanion.restore(chat.bufnr)
return output_handler(response)
end, wait_opts)
else
log:debug("[Insert Edit Into File Tool] No user confirmation needed for file, returning success")
return output_handler(success)
end
end
---@class CodeCompanion.Tool.InsertEditIntoFile: CodeCompanion.Tools.Tool
return {
name = "insert_edit_into_file",
cmds = {
---Execute the edit commands
---@param self CodeCompanion.Tools
---@param args table The arguments from the LLM's tool call
---@param input? any The output from the previous function call
---@param output_handler function Async callback for completion
---@return nil
function(self, args, input, output_handler)
log:debug("[Insert Edit Into File Tool] Execution started for: %s", args.filepath)
local bufnr = buffers.get_bufnr_from_filepath(args.filepath)
if bufnr then
return edit_buffer(bufnr, self.chat, args, output_handler, self.tool.opts)
else
return edit_file(args, self.chat, output_handler, self.tool.opts)
end
end,
},
schema = {
type = "function",
["function"] = {
name = "insert_edit_into_file",
description = "Insert new code or modify existing code in a file. Use this tool once per file that needs to be modified, even if there are multiple edits for a file. The system is very smart and can understand how to apply your edits to the user's files if you follow the instructions.",
parameters = {
type = "object",
properties = {
explanation = {
type = "string",
description = "A short explanation of the code edit being made",
},
filepath = {
type = "string",
description = "The path to the file to edit, including its filename and extension",
},
code = {
type = "string",
description = "The code which follows the patch format",
},
},
required = {
"explanation",
"filepath",
"code",
},
additionalProperties = false,
},
},
},
system_prompt = PROMPT,
handlers = {
---The handler to determine whether to prompt the user for approval
---@param self CodeCompanion.Tool.InsertEditIntoFile
---@param tools CodeCompanion.Tools
---@param config table The tool configuration
---@return boolean
prompt_condition = function(self, tools, config)
local opts = config["insert_edit_into_file"].opts or {}
local args = self.args
local bufnr = buffers.get_bufnr_from_filepath(args.filepath)
if bufnr then
if opts.requires_approval.buffer then
return true
end
return false
end
if opts.requires_approval.file then
return true
end
return false
end,
---Resolve the patch algorithm to use
---@param tool CodeCompanion.Tool.InsertEditIntoFile
---@param tools CodeCompanion.Tools The tool object
setup = function(tool, tools)
patch = resolve_patch_module(tool.opts.patching_algorithm)
end,
---@param tools CodeCompanion.Tools The tool object
---@return nil
on_exit = function(tools)
log:trace("[Insert Edit Into File Tool] on_exit handler executed")
end,
},
output = {
---The message which is shared with the user when asking for their approval
---@param self CodeCompanion.Tool.InsertEditIntoFile
---@param tools CodeCompanion.Tools
---@return nil|string
prompt = function(self, tools)
local args = self.args
local filepath = vim.fn.fnamemodify(args.filepath, ":.")
return fmt("Edit the file at %s?", filepath)
end,
---@param self CodeCompanion.Tool.InsertEditIntoFile
---@param tools CodeCompanion.Tools
---@param cmd table The command that was executed
---@param stdout table The output from the command
success = function(self, tools, cmd, stdout)
local llm_output = vim.iter(stdout):flatten():join("\n")
tools.chat:add_tool_output(self, llm_output)
end,
---@param self CodeCompanion.Tool.InsertEditIntoFile
---@param tools CodeCompanion.Tools
---@param cmd table
---@param stderr table The error output from the command
error = function(self, tools, cmd, stderr)
local chat = tools.chat
local args = self.args
local errors = vim.iter(stderr):flatten():join("\n")
local error_output = fmt(
[[Error editing `%s`
%s]],
args.filepath,
errors
)
local llm_error_output = fmt(
[[%s
**Troubleshooting tips:**
- Ensure your patch uses the correct format with Begin/End markers
- Check that the context lines exactly match the file content
- Verify indentation and whitespace match precisely
- Try providing more unique context to improve matching confidence]],
error_output
)
chat:add_tool_output(self, llm_error_output, error_output)
end,
---Rejection message back to the LLM
---@param self CodeCompanion.Tool.InsertEditIntoFile
---@param tools CodeCompanion.Tools
---@param cmd table
---@return nil
rejected = function(self, tools, cmd)
local chat = tools.chat
chat:add_tool_output(self, fmt("User rejected to edit `%s`", self.args.filepath))
end,
},
}

View file

@ -1,290 +0,0 @@
local Utils = require("codecompanion.strategies.chat.tools.catalog.list_code_usages.utils")
local log = require("codecompanion.utils.log")
local api = vim.api
---@class ListCodeUsages.CodeExtractor
local CodeExtractor = {}
local CONSTANTS = {
MAX_BLOCK_SCAN_LINES = 100,
}
--- Finds the most appropriate code block using TreeSitter and locals queries
---
--- This function uses TreeSitter to find the most contextually relevant code block
--- around a given position. It leverages locals queries to find scopes and attempts
--- to return the smallest significant scope that contains the target position.
---
---@param bufnr number The buffer number to extract from
---@param row number The row position (0-indexed)
---@param col number The column position (0-indexed)
---@return userdata|nil TreeSitter node representing the best code block, or nil if not found
function CodeExtractor.get_block_with_locals(bufnr, row, col)
local success, parser = pcall(vim.treesitter.get_parser, bufnr)
if not success or not parser then
return nil
end
local trees = parser:parse()
log:debug("[CodeExtractor:get_block_with_locals] Parsed %d treesitter trees.", #trees)
if not trees or #trees == 0 then
return nil
end
-- Find the node at the cursor position
local tree = trees[1]
local root = tree:root()
local node = root:named_descendant_for_range(row, col, row, col)
if not node then
return nil
end
-- Get the locals query for this language
local query = vim.treesitter.query.get(parser:lang(), "locals")
if not query then
log:debug("[CodeExtractor:get_block_with_locals] No locals query for language: %s", parser:lang())
return node
end
-- Find all scopes in the file
local scopes = {}
local target_node = node
-- First pass: find all scopes and possibly the exact symbol node
for _, tree in ipairs(trees) do
for id, found_node, meta in query:iter_captures(tree:root(), bufnr) do
local capture_name = query.captures[id]
if capture_name == "local.scope" then
table.insert(scopes, {
node = found_node,
range = { found_node:range() },
})
end
end
end
log:debug("[CodeExtractor:get_block_with_locals] Found %d scopes in the file", #scopes)
-- Simple helper function to check node type
local function matches_any(node_type, patterns)
for _, pattern in ipairs(patterns) do
if node_type:match(pattern) then
return true
end
end
return false
end
-- Get target position for scope matching
local target_start_row, target_start_col, target_end_row, target_end_col = target_node:range()
-- Find the smallest scope that contains the target node
local best_scope = nil
local best_scope_size = math.huge
for _, scope in ipairs(scopes) do
local start_row, start_col, end_row, end_col = unpack(scope.range)
-- Check if the scope contains the target
if
(start_row < target_start_row or (start_row == target_start_row and start_col <= target_start_col))
and (end_row > target_end_row or (end_row == target_end_row and end_col >= target_end_col))
then
-- Calculate scope size (approximate number of characters)
local scope_size = (end_row - start_row) * 100 + (end_col - start_col)
-- Check if this scope is significant (function, class, etc.)
local scope_node_type = scope.node:type()
local is_significant = matches_any(scope_node_type, {
"module",
"namespace",
"class",
"interface",
"struct",
"impl",
"enum",
"constructor",
"function",
"expression_statement",
"method",
"procedure",
"def",
"type",
"const",
"field",
})
-- Only consider significant scopes, and prefer smaller ones
if is_significant and scope_size < best_scope_size then
best_scope = scope.node
best_scope_size = scope_size
log:debug(
"[CodeExtractor:get_block_with_locals] Found containing scope: %s (size: %d)",
scope_node_type,
scope_size
)
end
end
end
-- If we found a suitable scope, return it
if best_scope then
log:debug("[CodeExtractor:get_block_with_locals] Using best scope: %s", best_scope:type())
return best_scope
end
-- Walk up the tree to find the first significant enclosing block
local current = target_node
while current do
local current_type = current:type()
-- Check for function-like nodes
if matches_any(current_type, { "function", "method", "procedure", "def" }) then
log:debug("[CodeExtractor:get_block_with_locals] Found enclosing function: %s", current_type)
return current
end
-- Check for class-like nodes
if matches_any(current_type, { "class", "interface", "struct", "enum" }) then
log:debug("[CodeExtractor:get_block_with_locals] Found enclosing class: %s", current_type)
return current
end
-- Move up to parent
current = current:parent()
end
-- If we didn't find a significant block, return the original node
return target_node
end
--- Extracts code text and metadata from a TreeSitter node
---
--- This function converts a TreeSitter node into a structured data object
--- containing the code text, line numbers, filename, and filetype information
--- needed for display in the tool output.
---
---@param bufnr number The buffer number containing the node
---@param node userdata TreeSitter node to extract data from
---@return table Result object with status and extracted code data
function CodeExtractor.extract_node_data(bufnr, node)
local start_row, start_col, end_row, end_col = node:range()
local lines = Utils.safe_get_lines(bufnr, start_row, end_row + 1)
if not lines or #lines == 0 then
return Utils.create_result("error", "Symbol text range is empty.")
end
-- Adjust last line
lines[#lines] = lines[#lines]:sub(1, end_col)
local code_block = table.concat(lines, "\n")
local absolute_filename = Utils.safe_get_buffer_name(bufnr)
local relative_filename = Utils.make_relative_path(absolute_filename)
local filetype = Utils.safe_get_filetype(bufnr)
return Utils.create_result("success", {
code_block = code_block,
start_line = start_row + 1, -- 1-indexed line numbers
end_line = end_row + 1, -- 1-indexed line numbers
filename = relative_filename,
filetype = filetype,
})
end
--- Fallback code extraction using indentation-based heuristics
---
--- When TreeSitter is not available or doesn't provide useful results, this function
--- uses indentation patterns to determine code block boundaries. It scans upward and
--- downward from the target position to find lines with consistent indentation.
---
---@param bufnr number The buffer number to extract from
---@param row number The row position (0-indexed)
---@param col number The column position (0-indexed)
---@return table Result object with status and extracted code data
function CodeExtractor.get_fallback_code_block(bufnr, row, col)
local lines = Utils.safe_get_lines(bufnr, row, row + 1)
local line = lines[1]
if not line then
return Utils.create_result("error", "No text at specified position")
end
-- Simple indentation-based extraction
local indent_pattern = "^(%s*)"
local indent = line:match(indent_pattern):len()
-- Find start of block (going upward)
local start_row = row
for i = row - 1, 0, -1 do
local curr_lines = Utils.safe_get_lines(bufnr, i, i + 1)
local curr_line = curr_lines[1]
if not curr_line then
break
end
local curr_indent = curr_line:match(indent_pattern):len()
if curr_indent < indent and not curr_line:match("^%s*$") and not curr_line:match("^%s*[//#*-]") then
break
end
start_row = i
end
-- Find end of block (going downward)
local end_row = row
local total_lines = api.nvim_buf_line_count(bufnr)
for i = row + 1, math.min(row + CONSTANTS.MAX_BLOCK_SCAN_LINES, total_lines - 1) do
local curr_lines = Utils.safe_get_lines(bufnr, i, i + 1)
local curr_line = curr_lines[1]
if not curr_line then
break
end
local curr_indent = curr_line:match(indent_pattern):len()
if curr_indent < indent and not curr_line:match("^%s*$") then
break
end
end_row = i
end
-- Extract the code block
local extracted_lines = Utils.safe_get_lines(bufnr, start_row, end_row + 1)
local absolute_filename = Utils.safe_get_buffer_name(bufnr)
local relative_filename = Utils.make_relative_path(absolute_filename)
local filetype = Utils.safe_get_filetype(bufnr)
return Utils.create_result("success", {
code_block = table.concat(extracted_lines, "\n"),
start_line = start_row + 1,
end_line = end_row + 1,
filename = relative_filename,
filetype = filetype,
})
end
--- Main entry point for extracting code blocks at a specific position
---
--- This function orchestrates the code extraction process by first attempting
--- TreeSitter-based extraction and falling back to indentation-based extraction
--- if TreeSitter is not available or doesn't provide useful results.
---
---@param bufnr number The buffer number to extract from
---@param row number The row position (0-indexed)
---@param col number The column position (0-indexed)
---@return table Result object with status and extracted code data
function CodeExtractor.get_code_block_at_position(bufnr, row, col)
if not Utils.is_valid_buffer(bufnr) then
return Utils.create_result("error", "Invalid buffer id: " .. tostring(bufnr))
end
local node = CodeExtractor.get_block_with_locals(bufnr, row, col)
if node then
return CodeExtractor.extract_node_data(bufnr, node)
else
return CodeExtractor.get_fallback_code_block(bufnr, row, col)
end
end
return CodeExtractor

View file

@ -1,362 +0,0 @@
local LspHandler = require("codecompanion.strategies.chat.tools.catalog.list_code_usages.lsp_handler")
local ResultProcessor = require("codecompanion.strategies.chat.tools.catalog.list_code_usages.result_processor")
local SymbolFinder = require("codecompanion.strategies.chat.tools.catalog.list_code_usages.symbol_finder")
local Utils = require("codecompanion.strategies.chat.tools.catalog.list_code_usages.utils")
local api = vim.api
local fmt = string.format
---@class CodeCompanion.Tool.ListCodeUsages: CodeCompanion.Tools.Tool
---@field symbol_data table Storage for collected symbol data across different operations
---@field filetype string The detected filetype for syntax highlighting in output
local ListCodeUsagesTool = {}
local CONSTANTS = {
LSP_METHODS = {
definition = vim.lsp.protocol.Methods.textDocument_definition,
references = vim.lsp.protocol.Methods.textDocument_references,
implementations = vim.lsp.protocol.Methods.textDocument_implementation,
declaration = vim.lsp.protocol.Methods.textDocument_declaration,
type_definition = vim.lsp.protocol.Methods.textDocument_typeDefinition,
documentation = vim.lsp.protocol.Methods.textDocument_hover,
},
}
--- Asynchronously processes LSP symbols by navigating to each symbol location
--- and executing all LSP methods to gather comprehensive information.
---
--- This function handles the complex async flow of:
--- 1. Opening each symbol's file
--- 2. Setting cursor to symbol position
--- 3. Executing all LSP methods (definition, references, implementations, etc.)
--- 4. Collecting and processing results
---
---@param symbols table[] Array of symbol objects from LSP workspace symbol search
---@param state table Shared state containing symbol_data and filetype
---@param callback function Callback function called with total results count when complete
local function process_lsp_symbols_async(symbols, state, callback)
local results_count = 0
local completed_symbols = 0
local total_symbols = #symbols
if total_symbols == 0 then
callback(0)
return
end
for _, symbol in ipairs(symbols) do
local filepath = symbol.file
local line = symbol.range.start.line + 1 -- Convert to 1-indexed
local col = symbol.range.start.character
Utils.async_edit_file(filepath, function(edit_success)
if edit_success then
Utils.async_set_cursor(line, col, function(cursor_success)
if cursor_success then
local current_bufnr = api.nvim_get_current_buf()
local methods_to_process = {}
-- Collect all methods to process
for operation, method in pairs(CONSTANTS.LSP_METHODS) do
table.insert(methods_to_process, { operation = operation, method = method })
end
local completed_methods = 0
local total_methods = #methods_to_process
-- Process each LSP method
for _, method_info in ipairs(methods_to_process) do
LspHandler.execute_request_async(current_bufnr, method_info.method, function(lsp_result)
results_count = results_count
+ ResultProcessor.process_lsp_results(lsp_result, method_info.operation, state.symbol_data)
completed_methods = completed_methods + 1
if completed_methods == total_methods then
-- Save filetype for the output
if results_count > 0 and not state.filetype then
state.filetype = Utils.safe_get_filetype(current_bufnr)
end
completed_symbols = completed_symbols + 1
if completed_symbols == total_symbols then
callback(results_count)
end
end
end)
end
else
completed_symbols = completed_symbols + 1
if completed_symbols == total_symbols then
callback(results_count)
end
end
end)
else
completed_symbols = completed_symbols + 1
if completed_symbols == total_symbols then
callback(results_count)
end
end
end)
end
end
--- Asynchronously processes grep search results to gather symbol information.
---
--- When LSP doesn't find all symbol occurrences, this function processes the first
--- grep match and executes LSP methods from that position to gather additional data.
--- It also processes the entire quickfix list for comprehensive coverage.
---
---@param grep_result table|nil Grep result containing file, line, col, and qflist
---@param state table Shared state containing symbol_data and filetype
---@param callback function Callback function called with results count when complete
local function process_grep_results_async(grep_result, state, callback)
local results_count = 0
if not grep_result then
callback(results_count)
return
end
Utils.async_edit_file(grep_result.file, function(edit_success)
if edit_success then
Utils.async_set_cursor(grep_result.line, grep_result.col, function(cursor_success)
if cursor_success then
local current_bufnr = api.nvim_get_current_buf()
local methods_to_process = {}
-- Collect all methods to process
for operation, method in pairs(CONSTANTS.LSP_METHODS) do
table.insert(methods_to_process, { operation = operation, method = method })
end
local completed_methods = 0
local total_methods = #methods_to_process
-- Process each LSP method
for _, method_info in ipairs(methods_to_process) do
LspHandler.execute_request_async(current_bufnr, method_info.method, function(lsp_result)
results_count = results_count
+ ResultProcessor.process_lsp_results(lsp_result, method_info.operation, state.symbol_data)
completed_methods = completed_methods + 1
if completed_methods == total_methods then
-- Process quickfix list results
if grep_result.qflist then
results_count = results_count
+ ResultProcessor.process_quickfix_references(grep_result.qflist, state.symbol_data)
end
-- Save filetype if needed
if results_count > 0 and not state.filetype then
state.filetype = Utils.safe_get_filetype(current_bufnr)
end
callback(results_count)
end
end)
end
else
callback(results_count)
end
end)
else
callback(results_count)
end
end)
end
--- Extracts file extension from the context buffer to help focus search results.
---
--- This is used to filter grep searches to files of the same type as the context,
--- improving search relevance and performance.
---
---@param context_bufnr number Buffer number of the context buffer
---@return string File extension without the dot, or "*" if not determinable
local function get_file_extension(context_bufnr)
if not Utils.is_valid_buffer(context_bufnr) then
return ""
end
local filename = Utils.safe_get_buffer_name(context_bufnr)
local name_only = filename:match("([^/\\]+)$") or filename
return (name_only:match("%.([^%.]+)$")) or "*"
end
return {
name = "list_code_usages",
cmds = {
---@param self table Tool instance with access to chat context
---@param args table Arguments containing symbol_name and optional file_paths
---@param input any Input data (unused in this tool)
---@param output_handler function Handler for tool output results
function(self, args, input, output_handler)
local symbol_name = args.symbol_name
local file_paths = args.file_paths
local state = {
symbol_data = {},
filetype = "",
}
if not symbol_name or symbol_name == "" then
output_handler(Utils.create_result("error", "Symbol name is required and cannot be empty."))
return
end
-- Save current state of view
local context_winnr = self.chat.buffer_context.winnr
local context_bufnr = self.chat.buffer_context.bufnr
local chat_winnr = api.nvim_get_current_win()
-- Get file extension from context buffer if available
local file_extension = get_file_extension(context_bufnr)
-- Exit insert mode and switch focus to context window
vim.cmd("stopinsert")
api.nvim_set_current_win(context_winnr)
-- Start async processing
SymbolFinder.find_with_lsp_async(symbol_name, file_paths, function(all_lsp_symbols)
SymbolFinder.find_with_grep_async(symbol_name, file_extension, file_paths, function(grep_result)
local total_results = 0
local completed_processes = 0
local total_processes = 2 -- LSP symbols and grep results
local function finalize_results()
-- Process all qflist results separately after LSP and grep processing
local qflist = vim.fn.getqflist()
total_results = total_results + ResultProcessor.process_quickfix_references(qflist, state.symbol_data)
-- Handle case where we have no results
if total_results == 0 then
api.nvim_set_current_win(chat_winnr)
local filetype_msg = file_extension and (" in " .. file_extension .. " files") or ""
output_handler(
Utils.create_result(
"error",
"Symbol not found in workspace"
.. filetype_msg
.. ". Double check the spelling and tool usage instructions."
)
)
return
end
if Utils.is_valid_buffer(context_bufnr) then
pcall(api.nvim_set_current_buf, context_bufnr)
end
if chat_winnr and api.nvim_win_is_valid(chat_winnr) then
pcall(api.nvim_set_current_win, chat_winnr)
end
-- Store state for output handler
ListCodeUsagesTool.symbol_data = state.symbol_data
ListCodeUsagesTool.filetype = state.filetype
output_handler(Utils.create_result("success", "Tool executed successfully"))
end
process_lsp_symbols_async(all_lsp_symbols, state, function(lsp_results_count)
total_results = total_results + lsp_results_count
completed_processes = completed_processes + 1
if completed_processes == total_processes then
finalize_results()
end
end)
process_grep_results_async(grep_result, state, function(grep_results_count)
total_results = total_results + grep_results_count
completed_processes = completed_processes + 1
if completed_processes == total_processes then
finalize_results()
end
end)
end)
end)
end,
},
schema = {
type = "function",
["function"] = {
name = "list_code_usages",
description = [[
Request to list all usages (references, definitions, implementations etc) of a function, class, method, variable etc. Use this tool when
1. Looking for a sample implementation of an interface or class
2. Checking how a function is used throughout the codebase.
3. Including and updating all usages when changing a function, method, or constructor]],
parameters = {
type = "object",
properties = {
symbol_name = {
type = "string",
description = "The name of the symbol, such as a function name, class name, method name, variable name, etc.",
},
file_paths = {
type = "array",
description = "One or more file paths which likely contain the definition of the symbol. For instance the file which declares a class or function. This is optional but will speed up the invocation of this tool and improve the quality of its output.",
items = {
type = "string",
},
},
},
required = {
"symbol_name",
},
},
},
},
handlers = {
---@param _ any Unused parameter
---@param tools table The tools instance
on_exit = function(_, tools)
ListCodeUsagesTool.symbol_data = {}
ListCodeUsagesTool.filetype = ""
end,
},
output = {
---@param self table Tool instance containing args and other data
---@param tools table The tools instance
---@param cmd any Command data (unused)
---@param stdout any Standard output (unused)
---@return any Result of adding tool output to chat
success = function(self, tools, cmd, stdout)
local symbol = self.args.symbol_name
local chat_message_content = fmt("Searched for symbol `%s`\n", symbol)
for operation, code_blocks in pairs(ListCodeUsagesTool.symbol_data) do
chat_message_content = chat_message_content .. fmt("\n%s:\n", operation, symbol)
for _, code_block in ipairs(code_blocks) do
if operation == "documentation" then
chat_message_content = chat_message_content .. fmt("---\n%s\n", code_block.code_block)
else
chat_message_content = chat_message_content
.. fmt(
"\n---\n\nFilename: %s:%s-%s\n```%s\n%s\n```\n",
code_block.filename,
code_block.start_line,
code_block.end_line,
code_block.filetype or ListCodeUsagesTool.filetype,
code_block.code_block
)
end
end
end
return tools.chat:add_tool_output(self, chat_message_content)
end,
---@param self table Tool instance
---@param tools table The tools instance
---@param cmd any Command data (unused)
---@param stderr table Array of error messages
---@return any Result of adding error output to chat
error = function(self, tools, cmd, stderr)
return tools.chat:add_tool_output(self, tostring(stderr[1]))
end,
},
}

View file

@ -1,107 +0,0 @@
local Utils = require("codecompanion.strategies.chat.tools.catalog.list_code_usages.utils")
local log = require("codecompanion.utils.log")
---@class ListCodeUsages.LspHandler
local LspHandler = {}
local CONSTANTS = {
LSP_METHODS = {
references = vim.lsp.protocol.Methods.textDocument_references,
documentation = vim.lsp.protocol.Methods.textDocument_hover,
},
}
--- Filters LSP references to only include files within the current project
---
--- This function removes references from external dependencies, system files,
--- or other locations outside the current project directory to keep results
--- focused and relevant.
---
---@param references table[] Array of LSP reference objects with uri fields
---@return table[] Filtered array containing only project-local references
function LspHandler.filter_project_references(references)
local filtered_results = {}
for _, reference in ipairs(references) do
local uri = reference.uri
if uri then
local filepath = Utils.uri_to_filepath(uri)
if Utils.is_in_project(filepath) then
table.insert(filtered_results, reference)
end
end
end
log:debug(
"[LspHandler:filter_project_references] References filtered. Original: %d, Filtered: %d",
#references,
#filtered_results
)
return filtered_results
end
--- Asynchronously executes an LSP request across all capable clients
---
--- This function handles the complex process of:
--- 1. Finding all LSP clients that support the requested method
--- 2. Ensuring clients are attached to the target buffer
--- 3. Executing the request with proper position parameters
--- 4. Collecting and processing results from all clients
--- 5. Handling special cases like hover documentation and references
---
---@param bufnr number The buffer number to execute the request on
---@param method string The LSP method to execute (e.g., textDocument_references)
---@param callback function Callback called with collected results from all clients
function LspHandler.execute_request_async(bufnr, method, callback)
local clients = vim.lsp.get_clients({ method = method })
local lsp_results = {}
local completed_clients = 0
local total_clients = #clients
if total_clients == 0 then
callback({})
return
end
for _, client in ipairs(clients) do
if not vim.lsp.buf_is_attached(bufnr, client.id) then
log:debug(
"[LspHandler:execute_request_async] Attaching client %s to buffer %d for method %s",
client.name,
bufnr,
method
)
vim.lsp.buf_attach_client(bufnr, client.id)
end
local position_params = vim.lsp.util.make_position_params(0, client.offset_encoding)
position_params.context = { includeDeclaration = false }
client:request(method, position_params, function(_, result, _, _)
if result then
-- Handle hover documentation specially
if method == CONSTANTS.LSP_METHODS.documentation and result.contents then
result = {
range = result.range,
contents = result.contents.value or result.contents,
}
end
-- For references, filter to just project references
if method == CONSTANTS.LSP_METHODS.references and type(result) == "table" then
lsp_results[client.name] = LspHandler.filter_project_references(result)
else
lsp_results[client.name] = result
end
end
completed_clients = completed_clients + 1
if completed_clients == total_clients then
callback(lsp_results)
end
end, bufnr)
end
end
return LspHandler

View file

@ -1,227 +0,0 @@
local CodeExtractor = require("codecompanion.strategies.chat.tools.catalog.list_code_usages.code_extractor")
local Utils = require("codecompanion.strategies.chat.tools.catalog.list_code_usages.utils")
local log = require("codecompanion.utils.log")
local api = vim.api
---@class ListCodeUsages.ResultProcessor
local ResultProcessor = {}
--- Checks if a new code block is a duplicate or enclosed by existing blocks
---
--- This function prevents redundant code blocks from being added to the results
--- by checking if the new block is identical to or completely contained within
--- any existing block across all operation types.
---
---@param new_block table Code block with filename, start_line, end_line fields
---@param symbol_data table Existing symbol data organized by operation type
---@return boolean True if the block is a duplicate or enclosed by an existing block
function ResultProcessor.is_duplicate_or_enclosed(new_block, symbol_data)
for _, blocks in pairs(symbol_data) do
for _, existing_block in ipairs(blocks) do
-- Skip if missing filename or line data (like documentation)
if not (existing_block.filename and existing_block.start_line and existing_block.end_line) then
goto continue
end
if
new_block.filename == existing_block.filename
and new_block.start_line == existing_block.start_line
and new_block.end_line == existing_block.end_line
then
log:debug(
"[ResultProcessor:is_duplicate_or_enclosed] Found exact duplicate: %s:%d-%d",
existing_block.filename,
existing_block.start_line,
existing_block.end_line
)
return true
end
if Utils.is_enclosed_by(new_block, existing_block) then
log:debug(
"[ResultProcessor:is_duplicate_or_enclosed] Found enclosed block: %s:%d-%d is enclosed by %s:%d-%d",
new_block.filename,
new_block.start_line,
new_block.end_line,
existing_block.filename,
existing_block.start_line,
existing_block.end_line
)
return true
end
::continue::
end
end
return false
end
--- Processes a single LSP result item and extracts its code block
---
--- This function takes an LSP result item (with URI and range) and extracts
--- the corresponding code block using the CodeExtractor. It handles deduplication
--- and adds the result to the appropriate operation category.
---
---@param uri string The file URI from the LSP result
---@param range table LSP range object with start/end positions
---@param operation string The type of LSP operation (e.g., "references", "definition")
---@param symbol_data table Symbol data storage organized by operation type
---@return table Result object indicating success or failure
function ResultProcessor.process_lsp_item(uri, range, operation, symbol_data)
if not (uri and range) then
return Utils.create_result("error", "Missing uri or range")
end
local target_bufnr = vim.uri_to_bufnr(uri)
vim.fn.bufload(target_bufnr)
local symbol_result = CodeExtractor.get_code_block_at_position(target_bufnr, range.start.line, range.start.character)
if symbol_result.status ~= "success" then
return symbol_result
end
if ResultProcessor.is_duplicate_or_enclosed(symbol_result.data, symbol_data) then
return Utils.create_result("success", "Duplicate or enclosed entry")
end
-- Add to results
if not symbol_data[operation] then
symbol_data[operation] = {}
end
table.insert(symbol_data[operation], symbol_result.data)
return Utils.create_result("success", "Symbol processed")
end
--- Processes documentation items from LSP hover responses
---
--- This function handles the special case of documentation/hover results,
--- which contain text content rather than code locations. It extracts the
--- documentation content and adds it to the results with deduplication.
---
---@param symbol_data table Symbol data storage organized by operation type
---@param operation string The operation type (typically "documentation")
---@param result table LSP hover result containing documentation content
---@return table Result object indicating success or failure
function ResultProcessor.process_documentation_item(symbol_data, operation, result)
if not symbol_data[operation] then
symbol_data[operation] = {}
end
local content = result.contents
-- jdtls puts documentation in a table where last element is content
if type(content) == "table" and type(content[#content]) == "string" then
content = content[#content]
end
-- Check for duplicates before adding documentation
local is_duplicate = false
for _, existing_item in ipairs(symbol_data[operation]) do
if existing_item.code_block == content then
is_duplicate = true
break
end
end
if not is_duplicate then
table.insert(symbol_data[operation], { code_block = content })
end
return Utils.create_result("success", "documentation processed")
end
--- Processes LSP results from multiple clients for a specific operation
---
--- This function handles the complex task of processing LSP results that can come
--- in various formats (single items, arrays, documentation) from multiple LSP clients.
--- It delegates to appropriate processing functions based on the result structure.
---
---@param lsp_results table Results from LSP clients, organized by client name
---@param operation string The LSP operation type (e.g., "references", "definition")
---@param symbol_data table Symbol data storage organized by operation type
---@return number Count of successfully processed results
function ResultProcessor.process_lsp_results(lsp_results, operation, symbol_data)
local processed_count = 0
for _, result in pairs(lsp_results) do
-- Handle documentation specially
if result.contents then
local process_result = ResultProcessor.process_documentation_item(symbol_data, operation, result)
if process_result.status == "success" then
processed_count = processed_count + 1
end
-- Handle single item with range
elseif result.range then
local process_result =
ResultProcessor.process_lsp_item(result.uri or result.targetUri, result.range, operation, symbol_data)
if process_result.status == "success" then
processed_count = processed_count + 1
end
-- Handle array of items
else
for _, item in pairs(result) do
local process_result = ResultProcessor.process_lsp_item(
item.uri or item.targetUri,
item.range or item.targetSelectionRange,
operation,
symbol_data
)
if process_result.status == "success" and process_result.data ~= "Duplicate or enclosed entry" then
processed_count = processed_count + 1
end
end
end
end
return processed_count
end
--- Processes code references from the quickfix list (grep results)
---
--- This function processes grep search results stored in Neovim's quickfix list.
--- It extracts code blocks for each match and adds them to the symbol data with
--- proper deduplication. This provides broader coverage when LSP doesn't find
--- all symbol occurrences.
---
---@param qflist table Array of quickfix items from vim.fn.getqflist()
---@param symbol_data table Symbol data storage organized by operation type
---@return number Count of successfully processed quickfix items
function ResultProcessor.process_quickfix_references(qflist, symbol_data)
if not qflist or #qflist == 0 then
return 0
end
log:debug("[ResultProcessor:process_quickfix_references] Processing %d quickfix items", #qflist)
local processed_count = 0
for _, qfitem in ipairs(qflist) do
if qfitem.bufnr and qfitem.lnum then
local target_bufnr = qfitem.bufnr
local row = qfitem.lnum - 1 -- Convert to 0-indexed
local col = qfitem.col - 1 -- Convert to 0-indexed
-- Load buffer if needed
if not api.nvim_buf_is_loaded(target_bufnr) then
vim.fn.bufload(target_bufnr)
end
-- Extract code block using locals-enhanced treesitter
local symbol_result = CodeExtractor.get_code_block_at_position(target_bufnr, row, col)
if symbol_result.status == "success" then
if not ResultProcessor.is_duplicate_or_enclosed(symbol_result.data, symbol_data) then
-- Initialize references array if needed
if not symbol_data["grep"] then
symbol_data["grep"] = {}
end
table.insert(symbol_data["grep"], symbol_result.data)
processed_count = processed_count + 1
end
end
end
end
return processed_count
end
return ResultProcessor

View file

@ -1,145 +0,0 @@
local Utils = require("codecompanion.strategies.chat.tools.catalog.list_code_usages.utils")
local log = require("codecompanion.utils.log")
---@class ListCodeUsages.SymbolFinder
local SymbolFinder = {}
local CONSTANTS = {
--- Directories to exclude from grep searches to improve performance and relevance
EXCLUDED_DIRS = { "node_modules", "dist", "vendor", ".git", "venv", ".env", "target", "build" },
}
--- Asynchronously finds symbols using LSP workspace symbol search
---
--- This function queries all available LSP clients that support workspace symbol
--- search to find symbols matching the given name. It filters results by file paths
--- if provided and sorts them by symbol kind to prioritize definitions.
---
---@param symbolName string The name of the symbol to search for
---@param filepaths string[]|nil Optional array of file paths to filter results
---@param callback function Callback function called with array of found symbols
function SymbolFinder.find_with_lsp_async(symbolName, filepaths, callback)
local clients = vim.lsp.get_clients({
method = vim.lsp.protocol.Methods.workspace_symbol,
})
if #clients == 0 then
callback({})
return
end
local symbols = {}
local completed_clients = 0
local total_clients = #clients
for _, client in ipairs(clients) do
local params = { query = symbolName }
client:request(vim.lsp.protocol.Methods.workspace_symbol, params, function(err, result, _, _)
if result then
for _, symbol in ipairs(result) do
if symbol.name == symbolName then
local filepath = Utils.uri_to_filepath(symbol.location.uri)
-- Filter by filepaths if specified
if filepaths and #filepaths > 0 then
local match = false
for _, pattern in ipairs(filepaths) do
if filepath:find(pattern) then
match = true
break
end
end
if not match then
goto continue
end
end
table.insert(symbols, {
uri = symbol.location.uri,
range = symbol.location.range,
name = symbol.name,
kind = symbol.kind,
file = filepath,
})
::continue::
end
end
end
completed_clients = completed_clients + 1
if completed_clients == total_clients then
-- Sort symbols by kind to prioritize definitions
table.sort(symbols, function(a, b)
return (a.kind or 999) < (b.kind or 999)
end)
log:debug("[SymbolFinder:find_with_lsp_async] Found symbols with LSP:\n %s", vim.inspect(symbols))
callback(symbols)
end
end)
end
end
--- Asynchronously finds symbols using grep-based text search
---
--- This function performs a grep search for the symbol name, with optional filtering
--- by file extension and specific file paths. It uses Neovim's built-in grep functionality
--- and populates the quickfix list with results.
---
---@param symbolName string The name of the symbol to search for
---@param file_extension string|nil Optional file extension to limit search scope
---@param filepaths string[]|nil Optional array of file paths to search within
---@param callback function Callback called with grep result object or nil if no matches
function SymbolFinder.find_with_grep_async(symbolName, file_extension, filepaths, callback)
vim.schedule(function()
local search_pattern = vim.fn.escape(symbolName, "\\")
local cmd = "silent! grep! -w"
-- Add file extension filter if provided
if file_extension and file_extension ~= "" then
cmd = cmd .. " --glob=" .. vim.fn.shellescape("*." .. file_extension) .. " "
end
-- Add exclusion patterns for directories
for _, dir in ipairs(CONSTANTS.EXCLUDED_DIRS) do
cmd = cmd .. " --glob=!" .. vim.fn.shellescape(dir .. "/**") .. " "
end
cmd = cmd .. vim.fn.shellescape(search_pattern)
-- Add file paths if provided
if filepaths and type(filepaths) == "table" and #filepaths > 0 then
cmd = cmd .. " " .. table.concat(filepaths, " ")
end
log:debug("[SymbolFinder:find_with_grep_async] Executing grep command: %s", cmd)
local success, _ = pcall(vim.cmd, cmd)
if not success then
callback(nil)
return
end
local qflist = vim.fn.getqflist()
if #qflist == 0 then
callback(nil)
return
end
log:debug("[SymbolFinder:find_with_grep_async] Found grep matches: \n %s", vim.inspect(qflist))
local first_match = qflist[1]
callback({
file = vim.fn.bufname(first_match.bufnr),
line = first_match.lnum,
col = first_match.col,
text = first_match.text,
bufnr = first_match.bufnr,
qflist = qflist,
})
end)
end
return SymbolFinder

View file

@ -1,128 +0,0 @@
local api = vim.api
---@class ListCodeUsages.Utils
local Utils = {}
---@param status "success"|"error" The status of the operation
---@param data any The result data or error message
---@return table Result object with status and data fields
function Utils.create_result(status, data)
return { status = status, data = data }
end
---@param uri string|nil The file URI to convert
---@return string The local filesystem path, or empty string if uri is nil
function Utils.uri_to_filepath(uri)
return uri and uri:gsub("file://", "") or ""
end
---@param filepath string|nil The absolute filepath to convert
---@return string The relative path, filename only, or empty string if filepath is nil
function Utils.make_relative_path(filepath)
if not filepath or filepath == "" then
return ""
end
local cwd = vim.fn.getcwd()
-- Normalize paths to handle different separators
local normalized_cwd = cwd:gsub("\\", "/")
local normalized_filepath = filepath:gsub("\\", "/")
-- Ensure cwd ends with separator for proper matching
if not normalized_cwd:match("/$") then
normalized_cwd = normalized_cwd .. "/"
end
-- Check if filepath starts with cwd
if normalized_filepath:find(normalized_cwd, 1, true) == 1 then
-- Return relative path
return normalized_filepath:sub(#normalized_cwd + 1)
else
-- If not within cwd, return just the filename
return normalized_filepath:match("([^/]+)$") or normalized_filepath
end
end
---@param filepath string The absolute filepath to check
---@return boolean True if the file is within the project directory
function Utils.is_in_project(filepath)
local project_root = vim.fn.getcwd()
return filepath:find(project_root, 1, true) == 1
end
---@param bufnr number|nil The buffer number to validate
---@return boolean True if the buffer is valid and exists
function Utils.is_valid_buffer(bufnr)
return bufnr and api.nvim_buf_is_valid(bufnr)
end
---@param bufnr number The buffer number to get filetype from
---@return string The filetype string, or empty string if not available
function Utils.safe_get_filetype(bufnr)
if not Utils.is_valid_buffer(bufnr) then
return ""
end
local success, filetype = pcall(api.nvim_get_option_value, "filetype", { buf = bufnr })
return success and filetype or ""
end
---@param bufnr number The buffer number to get name from
---@return string The buffer name/filepath, or empty string if not available
function Utils.safe_get_buffer_name(bufnr)
if not Utils.is_valid_buffer(bufnr) then
return ""
end
local success, name = pcall(api.nvim_buf_get_name, bufnr)
return success and name or ""
end
---@param bufnr number The buffer number to get lines from
---@param start_row number The starting row (0-indexed)
---@param end_row number The ending row (0-indexed, exclusive)
---@param strict_indexing boolean|nil Whether to use strict indexing (optional)
---@return string[] Array of lines, or empty array if not available
function Utils.safe_get_lines(bufnr, start_row, end_row, strict_indexing)
if not Utils.is_valid_buffer(bufnr) then
return {}
end
local success, lines = pcall(api.nvim_buf_get_lines, bufnr, start_row, end_row, strict_indexing or false)
return success and lines or {}
end
---@param filepath string The path to the file to open
---@param callback function Callback function called with success boolean
function Utils.async_edit_file(filepath, callback)
vim.schedule(function()
local success, _ = pcall(vim.cmd, "edit " .. vim.fn.fnameescape(filepath))
callback(success)
end)
end
---@param line number The line number to position cursor at (1-indexed)
---@param col number The column number to position cursor at (0-indexed)
---@param callback function Callback function called with success boolean
function Utils.async_set_cursor(line, col, callback)
vim.schedule(function()
local success = pcall(api.nvim_win_set_cursor, 0, { line, col })
if success then
pcall(vim.cmd, "normal! zz")
end
callback(success)
end)
end
---@param block_a table Code block with filename, start_line, end_line fields
---@param block_b table Code block with filename, start_line, end_line fields
---@return boolean True if block_a is completely enclosed by block_b
function Utils.is_enclosed_by(block_a, block_b)
if block_a.filename ~= block_b.filename then
return false
end
return block_a.start_line >= block_b.start_line and block_a.end_line <= block_b.end_line
end
return Utils

View file

@ -1,103 +0,0 @@
local log = require("codecompanion.utils.log")
local api = vim.api
---@class CodeCompanion.Tool.NextEditSuggestion.Args
---@field filepath string
---@field line number
---@alias jump_action fun(path: string):number?
---@class CodeCompanion.Tool.NextEditSuggestion: CodeCompanion.Tools.Tool
return {
opts = {
---@type jump_action|string
jump_action = require("codecompanion.utils.ui").tabnew_reuse,
},
name = "next_edit_suggestion",
schema = {
type = "function",
["function"] = {
name = "next_edit_suggestion",
description = "Suggest a possible position in a file for the next edit.",
parameters = {
type = "object",
properties = {
filepath = {
type = "string",
description = "The relative path to the file to edit, including its filename and extension.",
},
line = {
type = "integer",
description = "Line number for the next edit (0-based). Use -1 if you're not sure about it.",
},
},
required = { "filepath", "line" },
additionalProperties = false,
},
},
},
system_prompt = function(_)
return [[# Next Edit Suggestion Tool
## CONTEXT
When you suggest a change to the codebase, you may call this tool to jump to the position in the file.
## OBJECTIVE
- Follow the tool's schema.
- Respond with a single command, per tool execution.
## RESPONSE
- Only use this tool when you have been given paths to the files
- DO NOT make up paths that you are not given
- Only use this tool when there's an unambiguous position to jump to
- If there are multiple possible edits, ask the users to make a choice before jumping
- Pass -1 as the line number if you are not sure about the correct line number
- Consider the paths as **CASE SENSITIVE**
]]
end,
cmds = {
---@param self CodeCompanion.Tools
---@param args CodeCompanion.Tool.NextEditSuggestion.Args
---@return {status: "success"|"error", data: string}
function(self, args, _)
if type(args.filepath) == "string" then
args.filepath = vim.fs.normalize(args.filepath)
end
local stat = vim.uv.fs_stat(args.filepath)
if stat == nil or stat.type ~= "file" then
log:error("failed to jump to %s", args.filepath)
if stat then
log:error("file stat:\n%s", vim.inspect(stat))
end
return { status = "error", data = "Invalid path: " .. tostring(args.filepath) }
end
if type(self.tool.opts.jump_action) == "string" then
local action_command = self.tool.opts.jump_action
---@type jump_action
self.tool.opts.jump_action = function(path)
vim.cmd(action_command .. " " .. path)
return api.nvim_get_current_win()
end
end
local winnr = self.tool.opts.jump_action(args.filepath)
if args.line >= 0 and winnr then
local ok = pcall(api.nvim_win_set_cursor, winnr, { args.line + 1, 0 })
if not ok then
local bufnr = api.nvim_win_get_buf(winnr)
return {
status = "error",
data = string.format(
"The jump to the file was successful, but This file only has %d lines. Unable to jump to line %d",
api.nvim_buf_line_count(bufnr),
args.line
),
}
end
end
return { status = "success", data = "Jump successful!" }
end,
},
}

View file

@ -1,210 +0,0 @@
local Path = require("plenary.path")
local helpers = require("codecompanion.strategies.chat.helpers")
local log = require("codecompanion.utils.log")
local fmt = string.format
---Read the contents of a file
---@param action {filepath: string, start_line_number_base_zero: number, end_line_number_base_zero: number} The action containing the filepath
---@return {status: "success"|"error", data: string}
local function read(action)
local filepath = helpers.validate_and_normalize_filepath(action.filepath)
local p = Path:new(filepath)
if not p:exists() or not p:is_file() then
return {
status = "error",
data = fmt("Error reading `%s`\nFile does not exist or is not a file", filepath),
}
end
local lines = p:readlines()
local start_line_zero = tonumber(action.start_line_number_base_zero)
local end_line_zero = tonumber(action.end_line_number_base_zero)
local error_msg = nil
if not start_line_zero then
error_msg = fmt(
[[Error reading `%s`
start_line_number_base_zero must be a valid number, got: %s]],
action.filepath,
tostring(action.start_line_number_base_zero)
)
elseif not end_line_zero then
error_msg = fmt(
[[Error reading `%s`
end_line_number_base_zero must be a valid number, got: %s]],
action.filepath,
tostring(action.end_line_number_base_zero)
)
elseif start_line_zero < 0 then
error_msg = fmt(
[[Error reading `%s`
start_line_number_base_zero cannot be negative, got: %d"]],
action.filepath,
start_line_zero
)
elseif end_line_zero < -1 then
error_msg = fmt(
[[Error reading `%s`
end_line_number_base_zero cannot be less than -1, got: %d]],
action.filepath,
end_line_zero
)
elseif start_line_zero >= #lines then
error_msg = fmt(
[[Error reading `%s`
start_line_number_base_zero (%d) is beyond file length. File `%s` has %d lines (0-%d)]],
action.filepath,
start_line_zero,
action.filepath,
#lines,
math.max(0, #lines - 1)
)
elseif end_line_zero ~= -1 and start_line_zero > end_line_zero then
error_msg = fmt(
[[Error reading `%s`
Invalid line range - start_line_number_base_zero (%d) comes after end_line_number_base_zero (%d)]],
action.filepath,
start_line_zero,
end_line_zero
)
end
if error_msg then
return {
status = "error",
data = fmt([[%s]], error_msg),
}
end
-- Clamp end_line_zero to the last valid line if it exceeds file length (unless -1)
if not error_msg and end_line_zero ~= -1 and end_line_zero >= #lines then
end_line_zero = math.max(0, #lines - 1)
end
-- Convert to 1-based indexing
local start_line = start_line_zero + 1
local end_line = end_line_zero == -1 and #lines or end_line_zero + 1
-- Extract the specified lines
local selected_lines = {}
for i = start_line, end_line do
table.insert(selected_lines, lines[i])
end
local content = table.concat(selected_lines, "\n")
local file_ext = vim.fn.fnamemodify(p.filename, ":e")
local output = fmt(
[[Read file `%s` from line %d to %d:
````%s
%s
````]],
action.filepath,
action.start_line_number_base_zero,
action.end_line_number_base_zero,
file_ext,
content
)
return {
status = "success",
data = output,
}
end
---@class CodeCompanion.Tool.ReadFile: CodeCompanion.Tools.Tool
return {
name = "read_file",
cmds = {
---Execute the file commands
---@param self CodeCompanion.Tool.ReadFile
---@param args table The arguments from the LLM's tool call
---@param input? any The output from the previous function call
---@return { status: "success"|"error", data: string }
function(self, args, input)
return read(args)
end,
},
schema = {
type = "function",
["function"] = {
name = "read_file",
description = "Read the contents of a file.\n\nYou must specify the line range you're interested in. If the file contents returned are insufficient for your task, you may call this tool again to retrieve more content.",
parameters = {
type = "object",
properties = {
filepath = {
type = "string",
description = "The relative path to the file to read, including its filename and extension.",
},
start_line_number_base_zero = {
type = "number",
description = "The line number to start reading from, 0-based.",
},
end_line_number_base_zero = {
type = "number",
description = "The inclusive line number to end reading at, 0-based. Use -1 to read until the end of the file.",
},
},
required = {
"filepath",
"start_line_number_base_zero",
"end_line_number_base_zero",
},
},
},
},
handlers = {
---@param tools CodeCompanion.Tools The tool object
---@return nil
on_exit = function(tools)
log:trace("[Read File Tool] on_exit handler executed")
end,
},
output = {
---The message which is shared with the user when asking for their approval
---@param self CodeCompanion.Tools.Tool
---@param tools CodeCompanion.Tools
---@return nil|string
prompt = function(self, tools)
local args = self.args
local filepath = vim.fn.fnamemodify(args.filepath, ":.")
return fmt("Read %s?", filepath)
end,
---@param self CodeCompanion.Tool.ReadFile
---@param tools CodeCompanion.Tools
---@param cmd table The command that was executed
---@param stdout table The output from the command
success = function(self, tools, cmd, stdout)
local chat = tools.chat
local llm_output = vim.iter(stdout):flatten():join("\n")
chat:add_tool_output(self, llm_output, fmt("Read file `%s`", self.args.filepath))
end,
---@param self CodeCompanion.Tool.ReadFile
---@param tools CodeCompanion.Tools
---@param cmd table
---@param stderr table The error output from the command
error = function(self, tools, cmd, stderr)
local chat = tools.chat
local args = self.args
local errors = vim.iter(stderr):flatten():join("\n")
log:debug("[Read File Tool] Error output: %s", stderr)
chat:add_tool_output(self, errors)
end,
---Rejection message back to the LLM
---@param self CodeCompanion.Tool.ReadFile
---@param tools CodeCompanion.Tools
---@param cmd table
---@return nil
rejected = function(self, tools, cmd)
local chat = tools.chat
chat:add_tool_output(self, "**Read File Tool**: The user declined to execute")
end,
},
}

View file

@ -1,127 +0,0 @@
local adapters = require("codecompanion.adapters")
local client = require("codecompanion.http")
local config = require("codecompanion.config")
local log = require("codecompanion.utils.log")
local fmt = string.format
---@class CodeCompanion.Tool.SearchWeb: CodeCompanion.Tools.Tool
return {
name = "search_web",
cmds = {
---@param self CodeCompanion.Tools The Editor tool
---@param args table The arguments from the LLM's tool call
---@param cb function Callback for asynchronous calls
---@return nil|{ status: "success"|"error", data: string }
function(self, args, _, cb)
local opts = self.tool.opts
if not opts or not opts.adapter then
log:error("[Search Web Tool] No adapter provided")
return cb({ status = "error", data = "No adapter for the search_web tool" })
end
if not args then
log:error("[Search Web Tool] No args provided")
return cb({ status = "error", data = "No args for the search_web tool" })
end
if not args.query or args.query == "" then
log:error("[Search Web Tool] No query provided")
return cb({ status = "error", data = "No query provided for the search_web tool" })
end
args.query = string.gsub(args.query, "%f[%w_]search_web%f[^%w_]", "", 1)
local tool_adapter = config.strategies.chat.tools.search_web.opts.adapter
local adapter = vim.deepcopy(adapters.resolve(tool_adapter))
adapter.methods.tools.search_web.setup(adapter, opts.opts, args)
local query = args.query
client
.new({
adapter = adapter,
})
:request(_, {
callback = function(err, data)
local error_message = [[Error searching for `%s`]]
local error_message_expanded = error_message .. "\n%s"
if err then
log:error("[Search Web Tool] " .. error_message, query)
return cb({ status = "error", data = fmt(error_message_expanded, query, err) })
end
if data then
local output = adapter.methods.tools.search_web.callback(adapter, data)
if output.status == "error" then
log:error("[Search Web Tool] " .. error_message, query)
return cb({ status = "error", data = fmt(error_message_expanded, query, output.content) })
end
return cb({ status = "success", data = output.content })
end
end,
})
end,
},
schema = {
type = "function",
["function"] = {
name = "search_web",
description = "Searches the web for a given query and returns the results.",
parameters = {
type = "object",
properties = {
query = {
type = "string",
description = "The query to search the web for.",
},
domains = {
type = "array",
items = {
type = "string",
},
description = "An array of domains to search from. You can leave this as an empty string and the search will be performed across all domains.",
},
},
required = { "query", "domains" },
},
},
},
output = {
---@param self CodeCompanion.Tool.SearchWeb
---@param tools CodeCompanion.Tools
---@param cmd table The command that was executed
---@param stdout table The output from the command
success = function(self, tools, cmd, stdout)
local chat = tools.chat
local content = vim
.iter(stdout[1])
:map(function(result)
return fmt([[<attachment url="%s" title="%s">%s</attachment>]], result.url, result.title, result.content)
end)
:totable()
local length = #content
local llm_output = fmt([[%s]], table.concat(content, "\n"))
local user_output = fmt([[Searched for `%s`, %d result(s)]], cmd.query, length)
chat:add_tool_output(self, llm_output, user_output)
end,
---@param self CodeCompanion.Tool.SearchWeb
---@param tools CodeCompanion.Tools
---@param stderr table The error output from the command
error = function(self, tools, _, stderr, _)
local chat = tools.chat
local args = self.args
log:debug("[Search Web Tool] Error output: %s", stderr)
local error_output = fmt([[Error searching for `%s`]], args.query)
chat:add_tool_output(self, error_output)
end,
},
}

View file

@ -2,7 +2,7 @@ local Terminal = {}
Terminal.close_augroup = "close_augroup"
Terminal.set_keymaps = function(winnr, bufnr)
Terminal.set_keymaps = function(bufnr)
local opts = { buffer = bufnr }
vim.keymap.set("t", "<c-\\><c-\\>", [[<C-\><C-n>]], opts)
@ -14,10 +14,10 @@ Terminal.set_keymaps = function(winnr, bufnr)
vim.keymap.set("t", "<C-w>", [[<C-\><C-n><C-w>]], opts)
vim.keymap.set({ "n" }, { "<A-q>", "<leader>q", "<space>q" }, function()
vim.api.nvim_win_close(winnr, true)
vim.api.nvim_win_close(vim.api.nvim_get_current_win(), true)
end, opts)
vim.keymap.set({ "t" }, { "<A-q>" }, function()
vim.api.nvim_win_close(winnr, true)
vim.api.nvim_win_close(vim.api.nvim_get_current_win(), true)
end, opts)
end
@ -58,13 +58,12 @@ vim.api.nvim_create_autocmd({ "TermOpen" }, {
return
end
Terminal.configure()
local winnr = vim.api.nvim_get_current_win()
Terminal.set_keymaps(winnr, params.buf)
Terminal.set_keymaps(params.buf)
vim.api.nvim_create_autocmd({ "TermClose" }, {
callback = function()
vim.schedule(function()
vim.api.nvim_buf_delete(params.buf, { force = true })
pcall(vim.api.nvim_buf_delete, params.buf, { force = true })
end)
vim.cmd("let &stl = &stl") -- redrawstatus | redrawtabline
end,

View file

@ -1,236 +0,0 @@
if vim.g.did_load_avante_plugin or vim.g.did_load_ai_plugin then
return
end
vim.g.did_load_avante_plugin = true
local g = vim.g
local opts = {
mode = "agentic",
rules = {
project_dir = ".avante/rules", -- relative to project root, can also be an absolute path
global_dir = "~/.config/avante/rules", -- absolute path
},
-- debug = true,
web_search_engine = {},
behaviour = {
auto_set_keymaps = true,
auto_set_highlight_group = true,
auto_focus_sidebar = true,
auto_suggestions = false,
auto_suggestions_respect_ignore = true,
auto_apply_diff_after_generation = false,
jump_result_buffer_on_finish = false,
support_paste_from_clipboard = true,
minimize_diff = true,
enable_token_counting = true,
use_cwd_as_project_root = true,
auto_focus_on_diff_view = true,
auto_approve_tool_permissions = false,
auto_add_current_file = true,
confirmation_ui_style = "popup",
},
selection = {
enabled = false,
hint_display = "delayed",
},
provider = "ollama",
providers = {
ollama = {
endpoint = g.ollama_url,
model = "orieg/gemma3-tools:4b",
extra_request_body = {
options = {
num_ctx = 131072,
},
},
},
openrouter = {
__inherited_from = "openai",
endpoint = "https://openrouter.ai/api/v1",
api_key_name = "OPENROUTER_API_KEY",
model = "openai/gpt-oss-20b:free",
},
airun = {
__inherited_from = "openai",
endpoint = g.airun_endpoint,
api_key_name = "AI_RUN_TOKEN",
model = g.airun_model,
allow_insecure = true,
extra_request_body = {
temperature = 0.7,
max_tokens = 512,
},
},
airun_autocomplete = {
__inherited_from = "openai",
endpoint = g.airun_endpoint,
allow_insecure = true,
api_key_name = "AI_RUN_TOKEN",
model = g.airun_autocomplete_model,
},
},
rag_service = { -- RAG Service configuration
enabled = false,
host_mount = os.getenv("HOME"),
runner = "nix", -- Runner for the RAG service (can use docker or nix)
docker_extra_args = "",
llm = {
provider = "ollama",
endpoint = g.ollama_url,
api_key = "",
model = "orieg/gemma3-tools:4b",
extra_request_body = {
options = {
num_ctx = 131072,
},
},
},
embed = {
provider = "ollama",
endpoint = g.ollama_url,
api_key = "",
model = "embeddinggemma:latest",
extra_request_body = {
options = {
embed_batch_size = 10,
},
},
},
},
mappings = {
suggestion = {
accept = "<Tab>",
next = "<M-]>",
prev = "<M-[>",
dismiss = "<C-]>",
},
diff = {
ours = "gH",
theirs = "gh",
all_theirs = "gA",
both = "gB",
cursor = "gc",
next = "]x",
prev = "[x",
},
jump = {
next = "]]",
prev = "[[",
},
submit = {
normal = "<CR>",
insert = "<C-s>",
},
cancel = {
normal = { "<C-c>", "<Esc>", "q" },
insert = { "<C-c>" },
},
ask = "<leader>aa",
new_ask = "<leader>an",
zen_mode = "<leader>az",
edit = "<leader>ae",
refresh = "<leader>ar",
focus = "<leader>af",
stop = "<leader>aS",
toggle = {
default = "<leader>at",
debug = "<leader>ad",
selection = "<leader>aC",
suggestion = "<leader>as",
repomap = "<leader>aR",
},
sidebar = {
expand_tool_use = "<S-Tab>",
next_prompt = "<up>",
prev_prompt = "<down>",
apply_all = "A",
apply_cursor = "a",
retry_user_request = "r",
edit_user_request = "e",
switch_windows = "<Tab>",
reverse_switch_windows = "<S-Tab>",
toggle_code_window = "x",
remove_file = "d",
add_file = "@",
close = { "q" },
---@alias AvanteCloseFromInput { normal: string | nil, insert: string | nil }
---@type AvanteCloseFromInput | nil
close_from_input = nil, -- e.g., { normal = "<Esc>", insert = "<C-d>" }
---@alias AvanteToggleCodeWindowFromInput { normal: string | nil, insert: string | nil }
---@type AvanteToggleCodeWindowFromInput | nil
toggle_code_window_from_input = nil, -- e.g., { normal = "x", insert = "<C-;>" }
},
files = {
add_current = "<leader>ac", -- Add current buffer to selected files
add_all_buffers = "<leader>aB", -- Add all buffer files to selected files
},
select_model = "<leader>a?", -- Select model command
select_history = "<leader>ah", -- Select history command
confirm = {
focus_window = "<C-w>f",
code = "c",
resp = "r",
input = "i",
},
},
windows = {
---@alias AvantePosition "right" | "left" | "top" | "bottom" | "smart"
---@type AvantePosition
position = "smart",
sidebar_header = {
enabled = true, -- true, false to enable/disable the header
align = "left", -- left, center, right for title
rounded = true,
},
edit = {
start_insert = false, -- Start insert mode when opening the edit window
},
ask = {
floating = false,
},
},
repo_map = {
ignore_patterns = {
"%.git",
"%.worktree",
"__pycache__",
"node_modules",
"target",
"build",
"dist",
"BUILD",
"vendor%.",
"%.min%.",
".devenv%.",
".direnv%.",
}, -- ignore files matching these
negate_patterns = {}, -- negate ignore files matching these.
},
selector = {
provider = "fzf_lua",
},
}
vim.api.nvim_create_user_command("AvanteAIRun", function(_)
opts.provider = "airun"
opts.auto_suggestions_provider = "airun_autocomplete"
require("avante").setup(opts)
end, {})
lze.load({
"avante.nvim",
event = vim.g.post_load_events,
on_require = { "avante", "avante_lib", "avante.api" },
after = function()
require("avante_lib").load()
require("avante").setup(opts)
end,
})

View file

@ -48,6 +48,9 @@ lze.load({
after = function()
local blink = require("blink-cmp")
local opts = {
term = {
enabled = true,
},
cmdline = {
enabled = true,
completion = { menu = { auto_show = false } },
@ -89,6 +92,7 @@ lze.load({
enabled = true,
},
trigger = {
prefetch_on_insert = true,
show_in_snippet = true,
show_on_keyword = true,
show_on_trigger_character = true,

View file

@ -1,105 +0,0 @@
vim.g.did_load_snacks_plugin = true
if vim.g.did_load_snacks_plugin then
return
end
vim.g.did_load_snacks_plugin = true
require("snacks").setup({
bigfile = { enabled = false },
dashboard = { enabled = false },
explorer = { enabled = false },
indent = { enabled = false },
picker = { enabled = false },
quickfile = { enabled = true },
scope = { enabled = false },
scroll = { enabled = false },
words = { enabled = false },
statuscolumn = {
enabled = false,
folds = {
open = true, -- show open fold icons
git_hl = false, -- use Git Signs hl for fold icons
},
refresh = 200, -- ms
},
styles = {
notification = {
wo = { wrap = true }, -- Wrap notifications
},
},
input = { enabled = true },
notifier = {
enabled = true,
timeout = 1000,
level = vim.log.levels.WARN,
},
})
vim.keymap.set("n", "<space>bd", function()
Snacks.bufdelete.delete()
end, { desc = "Buffers: delete current" })
vim.keymap.set("n", "<space>bc", function()
Snacks.bufdelete.other()
end, { desc = "Buffers: delete other" })
vim.keymap.set("n", "<leader>M", function()
Snacks.notifier.show_history()
end, { desc = "Notifications" })
vim.keymap.set("n", "<leader>N", function()
Snacks.win({
file = vim.api.nvim_get_runtime_file("doc/news.txt", false)[1],
width = 0.6,
height = 0.6,
wo = {
spell = false,
wrap = false,
signcolumn = "yes",
statuscolumn = " ",
conceallevel = 3,
},
})
end, { desc = "Neovim News" })
_G.dd = function(...)
Snacks.debug.inspect(...)
end
_G.bt = function()
Snacks.debug.backtrace()
end
vim.print = _G.dd -- Override print to use snacks for `:=` command
Snacks.toggle.option("hlsearch", { name = "HLSearch", global = true }):map("<leader>th")
Snacks.toggle.option("spell", { name = "Spelling" }):map("<leader>ts")
Snacks.toggle.option("wrap", { name = "Wrap" }):map("<leader>tw")
Snacks.toggle
.option("conceallevel", { off = 0, on = vim.o.conceallevel > 0 and vim.o.conceallevel or 2 })
:map("<leader>tc")
Snacks.toggle.treesitter():map("<leader>tT")
Snacks.toggle
.new({
id = "diagnostic_virtuallines",
name = "DiagnosticsVirtualLines",
get = function()
local current = vim.diagnostic.config()
if not current then
return false
end
if not current.virtual_lines then
return false
end
return current.virtual_lines.current_line
end,
set = function(state)
if state then
vim.diagnostic.config({
virtual_lines = { current_line = true },
})
else
vim.diagnostic.config({
virtual_lines = false,
})
end
end,
})
:map("<leader>td")

View file

@ -1,2 +1,3 @@
// create pattern Barrier for golang like python