remvoe snacks; remove avante
This commit is contained in:
parent
448cdca018
commit
f375a971b1
32 changed files with 469 additions and 4670 deletions
199
AGENTS.md
Normal file
199
AGENTS.md
Normal 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
160
README.md
|
|
@ -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` и файлы плагинов для примеров
|
||||
41
flake.lock
41
flake.lock
|
|
@ -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",
|
||||
|
|
|
|||
39
flake.nix
39
flake.nix
|
|
@ -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"
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
|
|
|||
85
nix/ai.nix
85
nix/ai.nix
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
'';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@
|
|||
, # Set by the overlay to ensure we use a compatible version of `wrapNeovimUnstable`
|
||||
wrapNeovimUnstable
|
||||
, neovimUtils
|
||||
, neovim-nightly
|
||||
,
|
||||
}:
|
||||
with lib;
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
// create pattern Barrier for golang like python
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue