feat: initial release of codetyper.nvim v0.2.0
AI-powered coding partner for Neovim with LLM integration. Features: - Split view for coder files (*.coder.*) and target files - Tag-based prompts with /@ and @/ syntax - Claude API and Ollama (local) LLM support - Smart prompt detection (refactor, add, document, explain) - Automatic code injection into target files - Project tree logging (.coder/tree.log) - Auto .gitignore management Ask Panel (chat interface): - Fixed at 1/4 screen width - File attachment with @ key - Ctrl+n for new chat - Ctrl+Enter to submit - Proper window close behavior - Navigation with Ctrl+h/j/k/l Commands: Coder, CoderOpen, CoderClose, CoderToggle, CoderProcess, CoderAsk, CoderTree, CoderTreeView
This commit is contained in:
88
CHANGELOG.md
Normal file
88
CHANGELOG.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.0] - 2026-01-11
|
||||
|
||||
### Added
|
||||
|
||||
- **Ask Panel** - Chat interface for asking questions about code
|
||||
- Fixed at 1/4 (25%) screen width for consistent layout
|
||||
- File attachment with `@` key (uses Telescope if available)
|
||||
- `Ctrl+n` to start a new chat (clears input and history)
|
||||
- `Ctrl+Enter` to submit questions
|
||||
- `Ctrl+f` to add current file as context
|
||||
- `Ctrl+h/j/k/l` for window navigation
|
||||
- `K/J` to jump between output and input windows
|
||||
- `Y` to copy last response to clipboard
|
||||
- `q` to close panel (closes both windows together)
|
||||
- Auto-open Ask panel on startup (configurable via `auto_open_ask`)
|
||||
- File content is now sent to LLM when attaching files with `@`
|
||||
|
||||
### Changed
|
||||
|
||||
- Ask panel width is now fixed at 25% (1/4 of screen)
|
||||
- Improved close behavior - closing either Ask window closes both
|
||||
- Proper focus management after closing Ask panel
|
||||
- Compact UI elements to fit 1/4 width layout
|
||||
- Changed "Assistant" label to "AI" in chat messages
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ask panel window state sync issues
|
||||
- Window focus returning to code after closing Ask panel
|
||||
- NerdTree/nvim-tree causing Ask panel to resize incorrectly
|
||||
|
||||
---
|
||||
|
||||
## [0.1.0] - 2026-01-11
|
||||
|
||||
### Added
|
||||
|
||||
- Initial release of Codetyper.nvim
|
||||
- Core plugin architecture with modular Lua structure
|
||||
- Split window view for coder and target files
|
||||
- Tag-based prompt system (`/@` to open, `@/` to close)
|
||||
- Claude API integration for code generation
|
||||
- Ollama API integration for local LLM support
|
||||
- Automatic `.gitignore` management for coder files and `.coder/` folder
|
||||
- Smart prompt type detection (refactor, add, document, explain)
|
||||
- Code injection system with multiple strategies
|
||||
- User commands: `Coder`, `CoderOpen`, `CoderClose`, `CoderToggle`, `CoderProcess`, `CoderTree`, `CoderTreeView`
|
||||
- Health check module (`:checkhealth codetyper`)
|
||||
- Comprehensive documentation and help files
|
||||
- Telescope integration for file selection (optional)
|
||||
- **Project tree logging**: Automatic `.coder/tree.log` maintenance
|
||||
- Updates on file create, save, delete
|
||||
- Debounced updates (1 second) for performance
|
||||
- File type icons for visual clarity
|
||||
- Ignores common build/dependency folders
|
||||
|
||||
### Configuration Options
|
||||
|
||||
- LLM provider selection (Claude/Ollama)
|
||||
- Window position and width customization
|
||||
- Custom prompt tag patterns
|
||||
- Auto gitignore toggle
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
### Legend
|
||||
|
||||
- **Added** - New features
|
||||
- **Changed** - Changes in existing functionality
|
||||
- **Deprecated** - Soon-to-be removed features
|
||||
- **Removed** - Removed features
|
||||
- **Fixed** - Bug fixes
|
||||
- **Security** - Vulnerability fixes
|
||||
|
||||
[Unreleased]: https://github.com/cargdev/codetyper.nvim/compare/v0.2.0...HEAD
|
||||
[0.2.0]: https://github.com/cargdev/codetyper.nvim/compare/v0.1.0...v0.2.0
|
||||
[0.1.0]: https://github.com/cargdev/codetyper.nvim/releases/tag/v0.1.0
|
||||
227
CONTRIBUTING.md
Normal file
227
CONTRIBUTING.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# Contributing to Codetyper.nvim
|
||||
|
||||
First off, thank you for considering contributing to Codetyper.nvim! 🎉
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Development Setup](#development-setup)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Making Changes](#making-changes)
|
||||
- [Submitting Changes](#submitting-changes)
|
||||
- [Style Guide](#style-guide)
|
||||
- [Testing](#testing)
|
||||
- [Questions](#questions)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project and everyone participating in it is governed by our commitment to creating a welcoming and inclusive environment. Please be respectful and constructive in all interactions.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Fork the repository
|
||||
2. Clone your fork locally
|
||||
3. Set up the development environment
|
||||
4. Create a branch for your changes
|
||||
5. Make your changes
|
||||
6. Submit a pull request
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Neovim >= 0.8.0
|
||||
- Lua 5.1+ or LuaJIT
|
||||
- Git
|
||||
|
||||
### Local Development
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/codetyper.nvim.git
|
||||
cd codetyper.nvim
|
||||
```
|
||||
|
||||
2. Create a minimal test configuration:
|
||||
```lua
|
||||
-- test/minimal_init.lua
|
||||
vim.opt.runtimepath:append(".")
|
||||
require("codetyper").setup({
|
||||
llm = {
|
||||
provider = "ollama", -- Use local for testing
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
3. Test your changes:
|
||||
```bash
|
||||
nvim --clean -u test/minimal_init.lua
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
codetyper.nvim/
|
||||
├── lua/
|
||||
│ └── codetyper/
|
||||
│ ├── init.lua # Main entry point
|
||||
│ ├── config.lua # Configuration management
|
||||
│ ├── types.lua # Type definitions
|
||||
│ ├── utils.lua # Utility functions
|
||||
│ ├── commands.lua # Command definitions
|
||||
│ ├── window.lua # Window/split management
|
||||
│ ├── parser.lua # Prompt tag parser
|
||||
│ ├── gitignore.lua # .gitignore management
|
||||
│ ├── autocmds.lua # Autocommands
|
||||
│ ├── inject.lua # Code injection
|
||||
│ ├── health.lua # Health check
|
||||
│ └── llm/
|
||||
│ ├── init.lua # LLM interface
|
||||
│ ├── claude.lua # Claude API client
|
||||
│ └── ollama.lua # Ollama API client
|
||||
├── plugin/
|
||||
│ └── codetyper.lua # Plugin loader
|
||||
├── doc/
|
||||
│ └── codetyper.txt # Vim help documentation
|
||||
├── README.md
|
||||
├── LICENSE
|
||||
├── CHANGELOG.md
|
||||
├── CONTRIBUTING.md
|
||||
└── llms.txt
|
||||
```
|
||||
|
||||
## Making Changes
|
||||
|
||||
### Branch Naming
|
||||
|
||||
Use descriptive branch names:
|
||||
- `feature/description` - New features
|
||||
- `fix/description` - Bug fixes
|
||||
- `docs/description` - Documentation updates
|
||||
- `refactor/description` - Code refactoring
|
||||
|
||||
### Commit Messages
|
||||
|
||||
Follow conventional commits:
|
||||
```
|
||||
type(scope): description
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
Types:
|
||||
- `feat` - New feature
|
||||
- `fix` - Bug fix
|
||||
- `docs` - Documentation
|
||||
- `style` - Formatting, no code change
|
||||
- `refactor` - Code restructuring
|
||||
- `test` - Adding tests
|
||||
- `chore` - Maintenance
|
||||
|
||||
Examples:
|
||||
```
|
||||
feat(llm): add support for GPT-4 API
|
||||
fix(parser): handle nested prompt tags
|
||||
docs(readme): update installation instructions
|
||||
```
|
||||
|
||||
## Submitting Changes
|
||||
|
||||
1. **Ensure your code follows the style guide**
|
||||
2. **Update documentation** if needed
|
||||
3. **Update CHANGELOG.md** for notable changes
|
||||
4. **Test your changes** thoroughly
|
||||
5. **Create a pull request** with:
|
||||
- Clear title describing the change
|
||||
- Description of what and why
|
||||
- Reference to any related issues
|
||||
|
||||
### Pull Request Template
|
||||
|
||||
```markdown
|
||||
## Description
|
||||
[Describe your changes]
|
||||
|
||||
## Type of Change
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature
|
||||
- [ ] Documentation update
|
||||
- [ ] Refactoring
|
||||
|
||||
## Testing
|
||||
[Describe how you tested your changes]
|
||||
|
||||
## Checklist
|
||||
- [ ] Code follows style guide
|
||||
- [ ] Documentation updated
|
||||
- [ ] CHANGELOG.md updated
|
||||
- [ ] All tests pass
|
||||
```
|
||||
|
||||
## Style Guide
|
||||
|
||||
### Lua Style
|
||||
|
||||
- Use 2 spaces for indentation
|
||||
- Use `snake_case` for variables and functions
|
||||
- Use `PascalCase` for module names
|
||||
- Add type annotations with `---@param`, `---@return`, etc.
|
||||
- Document public functions with LuaDoc comments
|
||||
|
||||
```lua
|
||||
---@mod module_name Module description
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Description of the function
|
||||
---@param name string The parameter description
|
||||
---@return boolean Success status
|
||||
function M.example_function(name)
|
||||
-- Implementation
|
||||
return true
|
||||
end
|
||||
|
||||
return M
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
- Keep README.md up to date
|
||||
- Update doc/codetyper.txt for new features
|
||||
- Use clear, concise language
|
||||
- Include examples where helpful
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing
|
||||
|
||||
1. Test all commands work correctly
|
||||
2. Test with different file types
|
||||
3. Test window management
|
||||
4. Test LLM integration (both Claude and Ollama)
|
||||
5. Test edge cases (empty files, large files, etc.)
|
||||
|
||||
### Health Check
|
||||
|
||||
Run `:checkhealth codetyper` to verify the plugin setup.
|
||||
|
||||
## Questions?
|
||||
|
||||
Feel free to:
|
||||
- Open an issue for bugs or feature requests
|
||||
- Start a discussion for questions
|
||||
- Reach out to the maintainer
|
||||
|
||||
## Contact
|
||||
|
||||
- **Maintainer**: cargdev
|
||||
- **Email**: carlos.gutierrez@carg.dev
|
||||
- **Website**: [cargdev.io](https://cargdev.io)
|
||||
- **Blog**: [blog.cargdev.io](https://blog.cargdev.io)
|
||||
|
||||
---
|
||||
|
||||
Thank you for contributing! 🙏
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 cargdev (Carlos Gutierrez)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
605
README.md
Normal file
605
README.md
Normal file
@@ -0,0 +1,605 @@
|
||||
# 🚀 Codetyper.nvim
|
||||
|
||||
**AI-powered coding partner for Neovim** - Write code faster with LLM assistance while staying in control of your logic.
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://neovim.io/)
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **🪟 Split View**: Work with your code and prompts side by side
|
||||
- **💬 Ask Panel**: Chat interface for questions and explanations (like avante.nvim)
|
||||
- **🏷️ Tag-based Prompts**: Use `/@` and `@/` tags to write natural language prompts
|
||||
- **🤖 Multiple LLM Providers**: Support for Claude API and Ollama (local)
|
||||
- **📝 Smart Injection**: Automatically detects prompt type (refactor, add, document)
|
||||
- **🔒 Git Integration**: Automatically adds `.coder.*` files and `.coder/` folder to `.gitignore`
|
||||
- **🌳 Project Tree Logging**: Automatically maintains a `tree.log` tracking your project structure
|
||||
- **⚡ Lazy Loading**: Only loads when you need it
|
||||
|
||||
---
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [Requirements](#-requirements)
|
||||
- [Installation](#-installation)
|
||||
- [Quick Start](#-quick-start)
|
||||
- [Configuration](#%EF%B8%8F-configuration)
|
||||
- [Commands Reference](#-commands-reference)
|
||||
- [Usage Guide](#-usage-guide)
|
||||
- [How It Works](#%EF%B8%8F-how-it-works)
|
||||
- [Keymaps](#-keymaps-suggested)
|
||||
- [Health Check](#-health-check)
|
||||
- [Contributing](#-contributing)
|
||||
|
||||
---
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- Neovim >= 0.8.0
|
||||
- curl (for API calls)
|
||||
- Claude API key **OR** Ollama running locally
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### Using [lazy.nvim](https://github.com/folke/lazy.nvim)
|
||||
|
||||
```lua
|
||||
{
|
||||
"cargdev/codetyper.nvim",
|
||||
cmd = { "Coder", "CoderOpen", "CoderToggle" },
|
||||
keys = {
|
||||
{ "<leader>co", "<cmd>Coder open<cr>", desc = "Coder: Open" },
|
||||
{ "<leader>ct", "<cmd>Coder toggle<cr>", desc = "Coder: Toggle" },
|
||||
{ "<leader>cp", "<cmd>Coder process<cr>", desc = "Coder: Process" },
|
||||
},
|
||||
config = function()
|
||||
require("codetyper").setup({
|
||||
llm = {
|
||||
provider = "claude", -- or "ollama"
|
||||
},
|
||||
})
|
||||
end,
|
||||
}
|
||||
```
|
||||
|
||||
### Using [packer.nvim](https://github.com/wbthomason/packer.nvim)
|
||||
|
||||
```lua
|
||||
use {
|
||||
"cargdev/codetyper.nvim",
|
||||
config = function()
|
||||
require("codetyper").setup()
|
||||
end,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
**1. Open a file and start Coder:**
|
||||
```vim
|
||||
:e src/utils.ts
|
||||
:Coder open
|
||||
```
|
||||
|
||||
**2. Write a prompt in the coder file (left panel):**
|
||||
```typescript
|
||||
/@ Create a function to validate email addresses
|
||||
using regex, return boolean @/
|
||||
```
|
||||
|
||||
**3. The LLM generates code and injects it into `utils.ts` (right panel)**
|
||||
|
||||
That's it! You're now coding with AI assistance. 🎉
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
```lua
|
||||
require("codetyper").setup({
|
||||
-- LLM Provider Configuration
|
||||
llm = {
|
||||
provider = "claude", -- "claude" or "ollama"
|
||||
|
||||
-- Claude (Anthropic) settings
|
||||
claude = {
|
||||
api_key = nil, -- Uses ANTHROPIC_API_KEY env var if nil
|
||||
model = "claude-sonnet-4-20250514",
|
||||
},
|
||||
|
||||
-- Ollama (local) settings
|
||||
ollama = {
|
||||
host = "http://localhost:11434",
|
||||
model = "codellama",
|
||||
},
|
||||
},
|
||||
|
||||
-- Window Configuration
|
||||
window = {
|
||||
width = 0.25, -- 25% of screen width (1/4) for Ask panel
|
||||
position = "left", -- "left" or "right"
|
||||
border = "rounded", -- Border style for floating windows
|
||||
},
|
||||
|
||||
-- Prompt Tag Patterns
|
||||
patterns = {
|
||||
open_tag = "/@", -- Tag to start a prompt
|
||||
close_tag = "@/", -- Tag to end a prompt
|
||||
file_pattern = "*.coder.*",
|
||||
},
|
||||
|
||||
-- Auto Features
|
||||
auto_gitignore = true, -- Automatically add coder files to .gitignore
|
||||
})
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `ANTHROPIC_API_KEY` | Your Claude API key (if not set in config) |
|
||||
|
||||
---
|
||||
|
||||
## 📜 Commands Reference
|
||||
|
||||
### Main Command
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `:Coder {subcommand}` | Main command with subcommands below |
|
||||
|
||||
### Subcommands
|
||||
|
||||
| Subcommand | Alias | Description |
|
||||
|------------|-------|-------------|
|
||||
| `open` | `:CoderOpen` | Open the coder split view for current file |
|
||||
| `close` | `:CoderClose` | Close the coder split view |
|
||||
| `toggle` | `:CoderToggle` | Toggle the coder split view on/off |
|
||||
| `process` | `:CoderProcess` | Process the last prompt and generate code |
|
||||
| `status` | - | Show plugin status and project statistics |
|
||||
| `focus` | - | Switch focus between coder and target windows |
|
||||
| `tree` | `:CoderTree` | Manually refresh the tree.log file |
|
||||
| `tree-view` | `:CoderTreeView` | Open tree.log in a readonly split |
|
||||
| `ask` | `:CoderAsk` | Open the Ask panel for questions |
|
||||
| `ask-toggle` | `:CoderAskToggle` | Toggle the Ask panel |
|
||||
| `ask-clear` | `:CoderAskClear` | Clear Ask chat history |
|
||||
|
||||
---
|
||||
|
||||
### Command Details
|
||||
|
||||
#### `:Coder open` / `:CoderOpen`
|
||||
|
||||
Opens a split view with:
|
||||
- **Left panel**: The coder file (`*.coder.*`) where you write prompts
|
||||
- **Right panel**: The target file where generated code is injected
|
||||
|
||||
```vim
|
||||
" If you have index.ts open:
|
||||
:Coder open
|
||||
" Creates/opens index.coder.ts on the left
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- If no file is in buffer, opens a file picker (Telescope if available)
|
||||
- Creates the coder file if it doesn't exist
|
||||
- Automatically sets the correct filetype for syntax highlighting
|
||||
|
||||
---
|
||||
|
||||
#### `:Coder close` / `:CoderClose`
|
||||
|
||||
Closes the coder split view, keeping only your target file open.
|
||||
|
||||
```vim
|
||||
:Coder close
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `:Coder toggle` / `:CoderToggle`
|
||||
|
||||
Toggles the coder view on or off. Useful for quick switching.
|
||||
|
||||
```vim
|
||||
:Coder toggle
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `:Coder process` / `:CoderProcess`
|
||||
|
||||
Processes the last completed prompt in the coder file and sends it to the LLM.
|
||||
|
||||
```vim
|
||||
" After writing a prompt and closing with @/
|
||||
:Coder process
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
1. Finds the last `/@...@/` prompt in the coder buffer
|
||||
2. Detects the prompt type (refactor, add, document, etc.)
|
||||
3. Sends it to the configured LLM with file context
|
||||
4. Injects the generated code into the target file
|
||||
|
||||
---
|
||||
|
||||
#### `:Coder status`
|
||||
|
||||
Displays current plugin status including:
|
||||
- LLM provider and configuration
|
||||
- API key status (configured/not set)
|
||||
- Window settings
|
||||
- Project statistics (files, directories)
|
||||
- Tree log path
|
||||
|
||||
```vim
|
||||
:Coder status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `:Coder focus`
|
||||
|
||||
Switches focus between the coder window and target window.
|
||||
|
||||
```vim
|
||||
:Coder focus
|
||||
" Press again to switch back
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `:Coder tree` / `:CoderTree`
|
||||
|
||||
Manually refreshes the `.coder/tree.log` file with current project structure.
|
||||
|
||||
```vim
|
||||
:Coder tree
|
||||
```
|
||||
|
||||
> Note: The tree is automatically updated on file save/create/delete.
|
||||
|
||||
---
|
||||
|
||||
#### `:Coder tree-view` / `:CoderTreeView`
|
||||
|
||||
Opens the tree.log file in a readonly split for viewing your project structure.
|
||||
|
||||
```vim
|
||||
:Coder tree-view
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `:Coder ask` / `:CoderAsk`
|
||||
|
||||
Opens the **Ask panel** - a chat interface similar to avante.nvim for asking questions about your code, getting explanations, or general programming help.
|
||||
|
||||
```vim
|
||||
:Coder ask
|
||||
```
|
||||
|
||||
**The Ask Panel Layout:**
|
||||
```
|
||||
┌───────────────────┬─────────────────────────────────────────┐
|
||||
│ 💬 Chat (output) │ │
|
||||
│ │ Your code file │
|
||||
│ ┌─ 👤 You ──── │ │
|
||||
│ │ What is this? │ │
|
||||
│ │ │
|
||||
│ ┌─ 🤖 AI ───── │ │
|
||||
│ │ This is... │ │
|
||||
├───────────────────┤ │
|
||||
│ ✏️ Input │ │
|
||||
│ Type question... │ │
|
||||
└───────────────────┴─────────────────────────────────────────┘
|
||||
(1/4 width) (3/4 width)
|
||||
```
|
||||
|
||||
> **Note:** The Ask panel is fixed at 1/4 (25%) of the screen width.
|
||||
|
||||
**Ask Panel Keymaps:**
|
||||
|
||||
| Key | Mode | Description |
|
||||
|-----|------|-------------|
|
||||
| `@` | Insert | Attach/reference a file |
|
||||
| `Ctrl+Enter` | Insert/Normal | Submit question |
|
||||
| `Ctrl+n` | Insert/Normal | Start new chat (clear all) |
|
||||
| `Ctrl+f` | Insert/Normal | Add current file as context |
|
||||
| `Ctrl+h/j/k/l` | Normal/Insert | Navigate between windows |
|
||||
| `q` | Normal | Close panel (closes both windows) |
|
||||
| `K` / `J` | Normal | Jump between output/input |
|
||||
| `Y` | Normal | Copy last response to clipboard |
|
||||
|
||||
---
|
||||
|
||||
#### `:Coder ask-toggle` / `:CoderAskToggle`
|
||||
|
||||
Toggles the Ask panel on or off.
|
||||
|
||||
```vim
|
||||
:Coder ask-toggle
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `:Coder ask-clear` / `:CoderAskClear`
|
||||
|
||||
Clears the Ask panel chat history.
|
||||
|
||||
```vim
|
||||
:Coder ask-clear
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 Usage Guide
|
||||
|
||||
### Step 1: Open Your Project File
|
||||
|
||||
Open any source file you want to work with:
|
||||
|
||||
```vim
|
||||
:e src/components/Button.tsx
|
||||
```
|
||||
|
||||
### Step 2: Start Coder View
|
||||
|
||||
```vim
|
||||
:Coder open
|
||||
```
|
||||
|
||||
This creates a split:
|
||||
```
|
||||
┌─────────────────────────┬─────────────────────────┐
|
||||
│ Button.coder.tsx │ Button.tsx │
|
||||
│ (write prompts here) │ (your actual code) │
|
||||
└─────────────────────────┴─────────────────────────┘
|
||||
```
|
||||
|
||||
### Step 3: Write Your Prompt
|
||||
|
||||
In the coder file (left), write your prompt using tags:
|
||||
|
||||
```tsx
|
||||
/@ Create a Button component with the following props:
|
||||
- variant: 'primary' | 'secondary' | 'danger'
|
||||
- size: 'sm' | 'md' | 'lg'
|
||||
- disabled: boolean
|
||||
- onClick: function
|
||||
Use Tailwind CSS for styling @/
|
||||
```
|
||||
|
||||
### Step 4: Process the Prompt
|
||||
|
||||
When you close the tag with `@/`, you'll be prompted to process. Or manually:
|
||||
|
||||
```vim
|
||||
:Coder process
|
||||
```
|
||||
|
||||
### Step 5: Review Generated Code
|
||||
|
||||
The generated code appears in your target file (right panel). Review, edit if needed, and save!
|
||||
|
||||
---
|
||||
|
||||
### Prompt Types
|
||||
|
||||
The plugin automatically detects what you want based on keywords:
|
||||
|
||||
| Keywords | Type | Behavior |
|
||||
|----------|------|----------|
|
||||
| `refactor`, `rewrite`, `change` | Refactor | Replaces code in target file |
|
||||
| `add`, `create`, `implement`, `new` | Add | Inserts code at cursor position |
|
||||
| `document`, `comment`, `jsdoc` | Document | Adds documentation above code |
|
||||
| `explain`, `what`, `how` | Explain | Shows explanation (no injection) |
|
||||
| *(other)* | Generic | Prompts you for injection method |
|
||||
|
||||
---
|
||||
|
||||
### Prompt Examples
|
||||
|
||||
#### Creating New Functions
|
||||
|
||||
```typescript
|
||||
/@ Create an async function fetchUsers that:
|
||||
- Takes a page number and limit as parameters
|
||||
- Fetches from /api/users endpoint
|
||||
- Returns typed User[] array
|
||||
- Handles errors gracefully @/
|
||||
```
|
||||
|
||||
#### Refactoring Code
|
||||
|
||||
```typescript
|
||||
/@ Refactor the handleSubmit function to:
|
||||
- Use async/await instead of .then()
|
||||
- Add proper TypeScript types
|
||||
- Extract validation logic into separate function @/
|
||||
```
|
||||
|
||||
#### Adding Documentation
|
||||
|
||||
```typescript
|
||||
/@ Add JSDoc documentation to all exported functions
|
||||
including @param, @returns, and @example tags @/
|
||||
```
|
||||
|
||||
#### Implementing Patterns
|
||||
|
||||
```typescript
|
||||
/@ Implement the singleton pattern for DatabaseConnection class
|
||||
with lazy initialization and thread safety @/
|
||||
```
|
||||
|
||||
#### Adding Tests
|
||||
|
||||
```typescript
|
||||
/@ Create unit tests for the calculateTotal function
|
||||
using Jest, cover edge cases:
|
||||
- Empty array
|
||||
- Negative numbers
|
||||
- Large numbers @/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ How It Works
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Neovim │
|
||||
├────────────────────────────┬────────────────────────────────────┤
|
||||
│ src/api.coder.ts │ src/api.ts │
|
||||
│ │ │
|
||||
│ /@ Create a REST client │ // Generated code appears here │
|
||||
│ class with methods for │ export class RestClient { │
|
||||
│ GET, POST, PUT, DELETE │ async get<T>(url: string) { │
|
||||
│ with TypeScript │ // ... │
|
||||
│ generics @/ │ } │
|
||||
│ │ } │
|
||||
└────────────────────────────┴────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── .coder/ # Auto-created, gitignored
|
||||
│ └── tree.log # Project structure log
|
||||
├── src/
|
||||
│ ├── index.ts # Your source file
|
||||
│ ├── index.coder.ts # Coder file (gitignored)
|
||||
│ ├── utils.ts
|
||||
│ └── utils.coder.ts
|
||||
└── .gitignore # Auto-updated with coder patterns
|
||||
```
|
||||
|
||||
### The Flow
|
||||
|
||||
1. **You write prompts** in `*.coder.*` files using `/@...@/` tags
|
||||
2. **Plugin detects** when you close a prompt tag
|
||||
3. **Context is gathered** from the target file (content, language, etc.)
|
||||
4. **LLM generates** code based on your prompt and context
|
||||
5. **Code is injected** into the target file based on prompt type
|
||||
6. **You review and save** - you're always in control!
|
||||
|
||||
### Project Tree Logging
|
||||
|
||||
The `.coder/tree.log` file is automatically maintained:
|
||||
|
||||
```
|
||||
# Project Tree: my-project
|
||||
# Generated: 2026-01-11 15:30:45
|
||||
# By: Codetyper.nvim
|
||||
|
||||
📦 my-project
|
||||
├── 📁 src
|
||||
│ ├── 📘 index.ts
|
||||
│ ├── 📘 utils.ts
|
||||
│ └── 📁 components
|
||||
│ └── ⚛️ Button.tsx
|
||||
├── 📋 package.json
|
||||
└── 📝 README.md
|
||||
```
|
||||
|
||||
Updated automatically when you:
|
||||
- Create new files
|
||||
- Save files
|
||||
- Delete files
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Keymaps (Suggested)
|
||||
|
||||
Add these to your Neovim config:
|
||||
|
||||
```lua
|
||||
-- Codetyper keymaps
|
||||
local map = vim.keymap.set
|
||||
|
||||
-- Coder view
|
||||
map("n", "<leader>co", "<cmd>Coder open<cr>", { desc = "Coder: Open view" })
|
||||
map("n", "<leader>cc", "<cmd>Coder close<cr>", { desc = "Coder: Close view" })
|
||||
map("n", "<leader>ct", "<cmd>Coder toggle<cr>", { desc = "Coder: Toggle view" })
|
||||
map("n", "<leader>cp", "<cmd>Coder process<cr>", { desc = "Coder: Process prompt" })
|
||||
map("n", "<leader>cs", "<cmd>Coder status<cr>", { desc = "Coder: Show status" })
|
||||
map("n", "<leader>cf", "<cmd>Coder focus<cr>", { desc = "Coder: Switch focus" })
|
||||
map("n", "<leader>cv", "<cmd>Coder tree-view<cr>", { desc = "Coder: View tree" })
|
||||
|
||||
-- Ask panel
|
||||
map("n", "<leader>ca", "<cmd>Coder ask<cr>", { desc = "Coder: Open Ask" })
|
||||
map("n", "<leader>cA", "<cmd>Coder ask-toggle<cr>", { desc = "Coder: Toggle Ask" })
|
||||
map("n", "<leader>cx", "<cmd>Coder ask-clear<cr>", { desc = "Coder: Clear Ask" })
|
||||
```
|
||||
|
||||
Or with [which-key.nvim](https://github.com/folke/which-key.nvim):
|
||||
|
||||
```lua
|
||||
local wk = require("which-key")
|
||||
wk.register({
|
||||
["<leader>c"] = {
|
||||
name = "+coder",
|
||||
o = { "<cmd>Coder open<cr>", "Open view" },
|
||||
c = { "<cmd>Coder close<cr>", "Close view" },
|
||||
t = { "<cmd>Coder toggle<cr>", "Toggle view" },
|
||||
p = { "<cmd>Coder process<cr>", "Process prompt" },
|
||||
s = { "<cmd>Coder status<cr>", "Show status" },
|
||||
f = { "<cmd>Coder focus<cr>", "Switch focus" },
|
||||
v = { "<cmd>Coder tree-view<cr>", "View tree" },
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Health Check
|
||||
|
||||
Verify your setup is correct:
|
||||
|
||||
```vim
|
||||
:checkhealth codetyper
|
||||
```
|
||||
|
||||
This checks:
|
||||
- ✅ Neovim version
|
||||
- ✅ curl availability
|
||||
- ✅ LLM configuration
|
||||
- ✅ API key status
|
||||
- ✅ Telescope availability (optional)
|
||||
- ✅ Gitignore configuration
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
## 👤 Author
|
||||
|
||||
**cargdev**
|
||||
|
||||
- 🌐 Website: [cargdev.io](https://cargdev.io)
|
||||
- 📝 Blog: [blog.cargdev.io](https://blog.cargdev.io)
|
||||
- 📧 Email: carlos.gutierrez@carg.dev
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
Made with ❤️ for the Neovim community
|
||||
</p>
|
||||
220
doc/codetyper.txt
Normal file
220
doc/codetyper.txt
Normal file
@@ -0,0 +1,220 @@
|
||||
*codetyper.txt* AI-powered coding partner for Neovim
|
||||
|
||||
Author: cargdev <carlos.gutierrez@carg.dev>
|
||||
Homepage: https://github.com/cargdev/codetyper.nvim
|
||||
License: MIT
|
||||
|
||||
==============================================================================
|
||||
CONTENTS *codetyper-contents*
|
||||
|
||||
1. Introduction ............................ |codetyper-introduction|
|
||||
2. Requirements ............................ |codetyper-requirements|
|
||||
3. Installation ............................ |codetyper-installation|
|
||||
4. Configuration ........................... |codetyper-configuration|
|
||||
5. Usage ................................... |codetyper-usage|
|
||||
6. Commands ................................ |codetyper-commands|
|
||||
7. Workflow ................................ |codetyper-workflow|
|
||||
8. API ..................................... |codetyper-api|
|
||||
|
||||
==============================================================================
|
||||
1. INTRODUCTION *codetyper-introduction*
|
||||
|
||||
Codetyper.nvim is an AI-powered coding partner that helps you write code
|
||||
faster using LLM APIs (Claude, Ollama) with a unique workflow.
|
||||
|
||||
Instead of generating files directly, Codetyper watches what you type in
|
||||
special `.coder.*` files and generates code when you close prompt tags.
|
||||
|
||||
Key features:
|
||||
- Split view with coder file and target file side by side
|
||||
- Prompt-based code generation using /@ ... @/ tags
|
||||
- Support for Claude and Ollama LLM providers
|
||||
- Automatic .gitignore management for coder files and .coder/ folder
|
||||
- Intelligent code injection based on prompt type
|
||||
- Automatic project tree logging in .coder/tree.log
|
||||
|
||||
==============================================================================
|
||||
2. REQUIREMENTS *codetyper-requirements*
|
||||
|
||||
- Neovim >= 0.8.0
|
||||
- curl (for API calls)
|
||||
- Claude API key (if using Claude) or Ollama running locally
|
||||
|
||||
==============================================================================
|
||||
3. INSTALLATION *codetyper-installation*
|
||||
|
||||
Using lazy.nvim: >lua
|
||||
|
||||
{
|
||||
"cargdev/codetyper.nvim",
|
||||
config = function()
|
||||
require("codetyper").setup({
|
||||
llm = {
|
||||
provider = "claude", -- or "ollama"
|
||||
claude = {
|
||||
api_key = vim.env.ANTHROPIC_API_KEY,
|
||||
},
|
||||
},
|
||||
})
|
||||
end,
|
||||
}
|
||||
<
|
||||
Using packer.nvim: >lua
|
||||
|
||||
use {
|
||||
"cargdev/codetyper.nvim",
|
||||
config = function()
|
||||
require("codetyper").setup()
|
||||
end,
|
||||
}
|
||||
<
|
||||
==============================================================================
|
||||
4. CONFIGURATION *codetyper-configuration*
|
||||
|
||||
Default configuration: >lua
|
||||
|
||||
require("codetyper").setup({
|
||||
llm = {
|
||||
provider = "claude", -- "claude" or "ollama"
|
||||
claude = {
|
||||
api_key = nil, -- Uses ANTHROPIC_API_KEY env var if nil
|
||||
model = "claude-sonnet-4-20250514",
|
||||
},
|
||||
ollama = {
|
||||
host = "http://localhost:11434",
|
||||
model = "codellama",
|
||||
},
|
||||
},
|
||||
window = {
|
||||
width = 0.4, -- 40% of screen width
|
||||
position = "left", -- "left" or "right"
|
||||
border = "rounded",
|
||||
},
|
||||
patterns = {
|
||||
open_tag = "/@",
|
||||
close_tag = "@/",
|
||||
file_pattern = "*.coder.*",
|
||||
},
|
||||
auto_gitignore = true,
|
||||
})
|
||||
<
|
||||
==============================================================================
|
||||
5. USAGE *codetyper-usage*
|
||||
|
||||
1. Open any file (e.g., `index.ts`)
|
||||
2. Run `:Coder open` to create/open the corresponding coder file
|
||||
3. In the coder file, write prompts using the tag syntax:
|
||||
>
|
||||
/@ Create a function that fetches user data from an API
|
||||
with error handling and returns a User object @/
|
||||
<
|
||||
4. When you close the tag with `@/`, the plugin will:
|
||||
- Send the prompt to the configured LLM
|
||||
- Generate the code
|
||||
- Inject it into the target file
|
||||
|
||||
==============================================================================
|
||||
6. COMMANDS *codetyper-commands*
|
||||
|
||||
*:Coder*
|
||||
:Coder [subcommand]
|
||||
Main command with subcommands:
|
||||
|
||||
open Open coder view for current file
|
||||
close Close coder view
|
||||
toggle Toggle coder view
|
||||
process Process the last prompt and generate code
|
||||
status Show plugin status and project statistics
|
||||
focus Switch focus between coder and target windows
|
||||
tree Manually refresh the tree.log file
|
||||
tree-view Open tree.log in a split view
|
||||
|
||||
*:CoderOpen*
|
||||
:CoderOpen
|
||||
Open the coder split view for the current file.
|
||||
|
||||
*:CoderClose*
|
||||
:CoderClose
|
||||
Close the coder split view.
|
||||
|
||||
*:CoderToggle*
|
||||
:CoderToggle
|
||||
Toggle the coder split view.
|
||||
|
||||
*:CoderProcess*
|
||||
:CoderProcess
|
||||
Process the last prompt in the current coder buffer and
|
||||
inject generated code into the target file.
|
||||
|
||||
*:CoderTree*
|
||||
:CoderTree
|
||||
Manually refresh the tree.log file in .coder/ folder.
|
||||
|
||||
*:CoderTreeView*
|
||||
:CoderTreeView
|
||||
Open the tree.log file in a vertical split for viewing.
|
||||
|
||||
==============================================================================
|
||||
7. WORKFLOW *codetyper-workflow*
|
||||
|
||||
The Coder Workflow~
|
||||
|
||||
1. Target File: Your actual source file (e.g., `src/utils.ts`)
|
||||
2. Coder File: A companion file (e.g., `src/utils.coder.ts`)
|
||||
|
||||
The coder file mirrors your target file's location and extension.
|
||||
When you write prompts in the coder file and close them, the
|
||||
generated code appears in the target file.
|
||||
|
||||
Prompt Types~
|
||||
|
||||
The plugin detects the type of request from your prompt:
|
||||
|
||||
- "refactor" - Modifies existing code
|
||||
- "add" / "create" / "implement" - Adds new code
|
||||
- "document" / "comment" - Adds documentation
|
||||
- "explain" - Provides explanations (no code injection)
|
||||
|
||||
Example Prompts~
|
||||
>
|
||||
/@ Refactor this function to use async/await @/
|
||||
|
||||
/@ Add input validation to the form handler @/
|
||||
|
||||
/@ Add JSDoc comments to all exported functions @/
|
||||
|
||||
/@ Create a React hook for managing form state
|
||||
with validation support @/
|
||||
<
|
||||
Project Tree Logging~
|
||||
|
||||
Codetyper automatically maintains a .coder/ folder with a tree.log file:
|
||||
>
|
||||
.coder/
|
||||
└── tree.log # Auto-updated project structure
|
||||
<
|
||||
The tree.log is updated whenever you:
|
||||
- Create a new file
|
||||
- Save a file
|
||||
- Delete a file
|
||||
- Change directories
|
||||
|
||||
View the tree anytime with `:Coder tree-view` or refresh with `:Coder tree`.
|
||||
|
||||
==============================================================================
|
||||
8. API *codetyper-api*
|
||||
|
||||
*codetyper.setup()*
|
||||
codetyper.setup({opts})
|
||||
Initialize the plugin with configuration options.
|
||||
|
||||
*codetyper.get_config()*
|
||||
codetyper.get_config()
|
||||
Returns the current configuration table.
|
||||
|
||||
*codetyper.is_initialized()*
|
||||
codetyper.is_initialized()
|
||||
Returns true if the plugin has been initialized.
|
||||
|
||||
==============================================================================
|
||||
vim:tw=78:ts=8:ft=help:norl:
|
||||
164
llms.txt
Normal file
164
llms.txt
Normal file
@@ -0,0 +1,164 @@
|
||||
# Codetyper.nvim - LLM Documentation
|
||||
|
||||
> This file helps LLMs understand the Codetyper.nvim plugin structure and functionality.
|
||||
|
||||
## Overview
|
||||
|
||||
Codetyper.nvim is a Neovim plugin written in Lua that acts as an AI-powered coding partner. It integrates with LLM APIs (Claude, Ollama) to help developers write code faster using a unique prompt-based workflow.
|
||||
|
||||
## Core Concept
|
||||
|
||||
Instead of having an AI generate entire files, Codetyper lets developers maintain control:
|
||||
|
||||
1. Developer opens a source file (e.g., `index.ts`)
|
||||
2. A companion "coder file" is created (`index.coder.ts`)
|
||||
3. Developer writes prompts using special tags: `/@ prompt @/`
|
||||
4. When the closing tag is typed, the LLM generates code
|
||||
5. Generated code is injected into the target file
|
||||
|
||||
## Plugin Architecture
|
||||
|
||||
```
|
||||
lua/codetyper/
|
||||
├── init.lua # Main entry, setup function, module initialization
|
||||
├── config.lua # Configuration management, defaults, validation
|
||||
├── types.lua # Lua type definitions for LSP/documentation
|
||||
├── utils.lua # Utility functions (file ops, notifications)
|
||||
├── commands.lua # Vim command definitions (:Coder, :CoderOpen, etc.)
|
||||
├── window.lua # Split window management (open, close, toggle)
|
||||
├── parser.lua # Parses /@ @/ tags from buffer content
|
||||
├── gitignore.lua # Manages .gitignore entries for coder files and .coder/ folder
|
||||
├── autocmds.lua # Autocommands for tag detection, filetype, tree updates
|
||||
├── inject.lua # Code injection strategies
|
||||
├── health.lua # Health check for :checkhealth
|
||||
├── tree.lua # Project tree logging (.coder/tree.log)
|
||||
└── llm/
|
||||
├── init.lua # LLM interface, provider selection
|
||||
├── claude.lua # Claude API client (Anthropic)
|
||||
└── ollama.lua # Ollama API client (local LLMs)
|
||||
```
|
||||
|
||||
## .coder/ Folder
|
||||
|
||||
The plugin automatically creates and maintains a `.coder/` folder in your project:
|
||||
|
||||
```
|
||||
.coder/
|
||||
└── tree.log # Project structure, auto-updated on file changes
|
||||
```
|
||||
|
||||
The `tree.log` contains:
|
||||
- Project name and timestamp
|
||||
- Full directory tree with file type icons
|
||||
- Automatically ignores: hidden files, node_modules, .git, build folders, coder files
|
||||
|
||||
Tree updates are triggered by:
|
||||
- `BufWritePost` - When files are saved
|
||||
- `BufNewFile` - When new files are created
|
||||
- `BufDelete` - When files are deleted
|
||||
- `DirChanged` - When changing directories
|
||||
|
||||
Updates are debounced (1 second) to prevent excessive writes.
|
||||
|
||||
## Key Functions
|
||||
|
||||
### Setup
|
||||
```lua
|
||||
require("codetyper").setup({
|
||||
llm = { provider = "claude" | "ollama", ... },
|
||||
window = { width = 0.4, position = "left" },
|
||||
patterns = { open_tag = "/@", close_tag = "@/" },
|
||||
auto_gitignore = true,
|
||||
})
|
||||
```
|
||||
|
||||
### Commands
|
||||
- `:Coder open` - Opens split view with coder file
|
||||
- `:Coder close` - Closes the split
|
||||
- `:Coder toggle` - Toggles the view
|
||||
- `:Coder process` - Manually triggers code generation
|
||||
- `:Coder status` - Shows configuration status and project stats
|
||||
- `:Coder tree` - Manually refresh tree.log
|
||||
- `:Coder tree-view` - Open tree.log in split view
|
||||
|
||||
### Prompt Tags
|
||||
- Opening tag: `/@`
|
||||
- Closing tag: `@/`
|
||||
- Content between tags is the prompt sent to LLM
|
||||
|
||||
### Prompt Types (Auto-detected)
|
||||
- `refactor` - Modifies existing code
|
||||
- `add` - Adds new code at cursor/end
|
||||
- `document` - Adds documentation/comments
|
||||
- `explain` - Explanations (no code injection)
|
||||
- `generic` - User chooses injection method
|
||||
|
||||
## File Naming Convention
|
||||
|
||||
| Target File | Coder File |
|
||||
|-------------|------------|
|
||||
| `index.ts` | `index.coder.ts` |
|
||||
| `utils.py` | `utils.coder.py` |
|
||||
| `main.lua` | `main.coder.lua` |
|
||||
|
||||
Pattern: `name.coder.extension`
|
||||
|
||||
## Configuration Schema
|
||||
|
||||
```lua
|
||||
{
|
||||
llm = {
|
||||
provider = "claude", -- "claude" | "ollama"
|
||||
claude = {
|
||||
api_key = nil, -- string, uses ANTHROPIC_API_KEY env if nil
|
||||
model = "claude-sonnet-4-20250514",
|
||||
},
|
||||
ollama = {
|
||||
host = "http://localhost:11434",
|
||||
model = "codellama",
|
||||
},
|
||||
},
|
||||
window = {
|
||||
width = 0.4, -- number (percentage if <=1, columns if >1)
|
||||
position = "left", -- "left" | "right"
|
||||
border = "rounded", -- border style for floating windows
|
||||
},
|
||||
patterns = {
|
||||
open_tag = "/@", -- string
|
||||
close_tag = "@/", -- string
|
||||
file_pattern = "*.coder.*",
|
||||
},
|
||||
auto_gitignore = true, -- boolean
|
||||
}
|
||||
```
|
||||
|
||||
## LLM Integration
|
||||
|
||||
### Claude API
|
||||
- Endpoint: `https://api.anthropic.com/v1/messages`
|
||||
- Uses `x-api-key` header for authentication
|
||||
- Requires `anthropic-version: 2023-06-01` header
|
||||
|
||||
### Ollama API
|
||||
- Endpoint: `{host}/api/generate`
|
||||
- No authentication required for local instances
|
||||
- Health check via `/api/tags` endpoint
|
||||
|
||||
## Code Injection Strategies
|
||||
|
||||
1. **Refactor**: Replace entire file content
|
||||
2. **Add**: Insert at cursor position in target file
|
||||
3. **Document**: Insert above current function/class
|
||||
4. **Generic**: Prompt user for action (replace/insert/append/clipboard)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Required**: Neovim >= 0.8.0, curl
|
||||
- **Optional**: telescope.nvim (enhanced file picker)
|
||||
|
||||
## Contact
|
||||
|
||||
- Author: cargdev
|
||||
- Email: carlos.gutierrez@carg.dev
|
||||
- Website: https://cargdev.io
|
||||
- Blog: https://blog.cargdev.io
|
||||
866
lua/codetyper/ask.lua
Normal file
866
lua/codetyper/ask.lua
Normal file
@@ -0,0 +1,866 @@
|
||||
---@mod codetyper.ask Ask window for Codetyper.nvim (similar to avante.nvim)
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
|
||||
---@class AskState
|
||||
---@field input_buf number|nil Input buffer
|
||||
---@field input_win number|nil Input window
|
||||
---@field output_buf number|nil Output buffer
|
||||
---@field output_win number|nil Output window
|
||||
---@field is_open boolean Whether the ask panel is open
|
||||
---@field history table Chat history
|
||||
---@field referenced_files table Files referenced with @
|
||||
|
||||
---@type AskState
|
||||
local state = {
|
||||
input_buf = nil,
|
||||
input_win = nil,
|
||||
output_buf = nil,
|
||||
output_win = nil,
|
||||
is_open = false,
|
||||
history = {},
|
||||
referenced_files = {},
|
||||
target_width = nil, -- Store the target width to maintain it
|
||||
}
|
||||
|
||||
--- Get the ask window configuration
|
||||
---@return table Config
|
||||
local function get_config()
|
||||
local ok, codetyper = pcall(require, "codetyper")
|
||||
if ok and codetyper.is_initialized() then
|
||||
return codetyper.get_config()
|
||||
end
|
||||
return {
|
||||
window = { width = 0.4, border = "rounded" },
|
||||
}
|
||||
end
|
||||
|
||||
--- Create the output buffer (chat history)
|
||||
---@return number Buffer number
|
||||
local function create_output_buffer()
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
|
||||
vim.bo[buf].buftype = "nofile"
|
||||
vim.bo[buf].bufhidden = "hide"
|
||||
vim.bo[buf].swapfile = false
|
||||
vim.bo[buf].filetype = "markdown"
|
||||
|
||||
-- Set initial content
|
||||
local header = {
|
||||
"╔═══════════════════════════════════╗",
|
||||
"║ 🤖 CODETYPER ASK ║",
|
||||
"╠═══════════════════════════════════╣",
|
||||
"║ Ask about code or concepts ║",
|
||||
"║ ║",
|
||||
"║ 💡 Keymaps: ║",
|
||||
"║ @ → attach file ║",
|
||||
"║ C-Enter → send ║",
|
||||
"║ C-n → new chat ║",
|
||||
"║ C-f → add current file ║",
|
||||
"║ C-h/j/k/l → navigate ║",
|
||||
"║ q → close │ K/J → jump ║",
|
||||
"╚═══════════════════════════════════╝",
|
||||
"",
|
||||
}
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, header)
|
||||
|
||||
return buf
|
||||
end
|
||||
|
||||
--- Create the input buffer
|
||||
---@return number Buffer number
|
||||
local function create_input_buffer()
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
|
||||
vim.bo[buf].buftype = "nofile"
|
||||
vim.bo[buf].bufhidden = "hide"
|
||||
vim.bo[buf].swapfile = false
|
||||
vim.bo[buf].filetype = "markdown"
|
||||
|
||||
-- Set placeholder text
|
||||
local placeholder = {
|
||||
"┌───────────────────────────────────┐",
|
||||
"│ 💬 Type your question here... │",
|
||||
"│ │",
|
||||
"│ @ attach │ C-Enter send │ C-n new │",
|
||||
"└───────────────────────────────────┘",
|
||||
}
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, placeholder)
|
||||
|
||||
return buf
|
||||
end
|
||||
|
||||
--- Setup keymaps for the input buffer
|
||||
---@param buf number Buffer number
|
||||
local function setup_input_keymaps(buf)
|
||||
local opts = { buffer = buf, noremap = true, silent = true }
|
||||
|
||||
-- Submit with Ctrl+Enter
|
||||
vim.keymap.set("i", "<C-CR>", function()
|
||||
M.submit()
|
||||
end, opts)
|
||||
|
||||
vim.keymap.set("n", "<C-CR>", function()
|
||||
M.submit()
|
||||
end, opts)
|
||||
|
||||
vim.keymap.set("n", "<CR>", function()
|
||||
M.submit()
|
||||
end, opts)
|
||||
|
||||
-- Include current file context with Ctrl+F
|
||||
vim.keymap.set({ "n", "i" }, "<C-f>", function()
|
||||
M.include_file_context()
|
||||
end, opts)
|
||||
|
||||
-- File picker with @
|
||||
vim.keymap.set("i", "@", function()
|
||||
M.show_file_picker()
|
||||
end, opts)
|
||||
|
||||
-- Close with q in normal mode
|
||||
vim.keymap.set("n", "q", function()
|
||||
M.close()
|
||||
end, opts)
|
||||
|
||||
-- Clear input with Ctrl+c
|
||||
vim.keymap.set("n", "<C-c>", function()
|
||||
M.clear_input()
|
||||
end, opts)
|
||||
|
||||
-- New chat with Ctrl+n (clears everything)
|
||||
vim.keymap.set({ "n", "i" }, "<C-n>", function()
|
||||
M.new_chat()
|
||||
end, opts)
|
||||
|
||||
-- Window navigation (works in both normal and insert mode)
|
||||
vim.keymap.set({ "n", "i" }, "<C-h>", function()
|
||||
vim.cmd("wincmd h")
|
||||
end, opts)
|
||||
|
||||
vim.keymap.set({ "n", "i" }, "<C-j>", function()
|
||||
vim.cmd("wincmd j")
|
||||
end, opts)
|
||||
|
||||
vim.keymap.set({ "n", "i" }, "<C-k>", function()
|
||||
vim.cmd("wincmd k")
|
||||
end, opts)
|
||||
|
||||
vim.keymap.set({ "n", "i" }, "<C-l>", function()
|
||||
vim.cmd("wincmd l")
|
||||
end, opts)
|
||||
|
||||
-- Jump to output window
|
||||
vim.keymap.set("n", "K", function()
|
||||
M.focus_output()
|
||||
end, opts)
|
||||
|
||||
-- When entering insert mode, clear placeholder
|
||||
vim.api.nvim_create_autocmd("InsertEnter", {
|
||||
buffer = buf,
|
||||
callback = function()
|
||||
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||
local content = table.concat(lines, "\n")
|
||||
if content:match("Type your question here") then
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "" })
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
--- Setup keymaps for the output buffer
|
||||
---@param buf number Buffer number
|
||||
local function setup_output_keymaps(buf)
|
||||
local opts = { buffer = buf, noremap = true, silent = true }
|
||||
|
||||
-- Close with q
|
||||
vim.keymap.set("n", "q", function()
|
||||
M.close()
|
||||
end, opts)
|
||||
|
||||
-- Clear history with Ctrl+c
|
||||
vim.keymap.set("n", "<C-c>", function()
|
||||
M.clear_history()
|
||||
end, opts)
|
||||
|
||||
-- New chat with Ctrl+n (clears everything)
|
||||
vim.keymap.set("n", "<C-n>", function()
|
||||
M.new_chat()
|
||||
end, opts)
|
||||
|
||||
-- Copy last response with Y
|
||||
vim.keymap.set("n", "Y", function()
|
||||
M.copy_last_response()
|
||||
end, opts)
|
||||
|
||||
-- Jump to input with i or J
|
||||
vim.keymap.set("n", "i", function()
|
||||
M.focus_input()
|
||||
end, opts)
|
||||
|
||||
vim.keymap.set("n", "J", function()
|
||||
M.focus_input()
|
||||
end, opts)
|
||||
|
||||
-- Window navigation
|
||||
vim.keymap.set("n", "<C-h>", function()
|
||||
vim.cmd("wincmd h")
|
||||
end, opts)
|
||||
|
||||
vim.keymap.set("n", "<C-j>", function()
|
||||
vim.cmd("wincmd j")
|
||||
end, opts)
|
||||
|
||||
vim.keymap.set("n", "<C-k>", function()
|
||||
vim.cmd("wincmd k")
|
||||
end, opts)
|
||||
|
||||
vim.keymap.set("n", "<C-l>", function()
|
||||
vim.cmd("wincmd l")
|
||||
end, opts)
|
||||
|
||||
end
|
||||
|
||||
--- Calculate window dimensions (always 1/4 of screen)
|
||||
---@return table Dimensions
|
||||
local function calculate_dimensions()
|
||||
-- Always use 1/4 of the screen width
|
||||
local width = math.floor(vim.o.columns * 0.25)
|
||||
|
||||
return {
|
||||
width = math.max(width, 30), -- Minimum 30 columns
|
||||
total_height = vim.o.lines - 4,
|
||||
output_height = vim.o.lines - 14,
|
||||
input_height = 8,
|
||||
}
|
||||
end
|
||||
|
||||
--- Autocmd group for maintaining width
|
||||
local ask_augroup = nil
|
||||
|
||||
--- Setup autocmd to always maintain 1/4 window width
|
||||
local function setup_width_autocmd()
|
||||
-- Clear previous autocmd group if exists
|
||||
if ask_augroup then
|
||||
pcall(vim.api.nvim_del_augroup_by_id, ask_augroup)
|
||||
end
|
||||
|
||||
ask_augroup = vim.api.nvim_create_augroup("CodetypeAskWidth", { clear = true })
|
||||
|
||||
-- Always maintain 1/4 width on any window event
|
||||
vim.api.nvim_create_autocmd({ "WinResized", "WinNew", "WinClosed", "VimResized" }, {
|
||||
group = ask_augroup,
|
||||
callback = function()
|
||||
if not state.is_open or not state.output_win then return end
|
||||
if not vim.api.nvim_win_is_valid(state.output_win) then return end
|
||||
|
||||
vim.schedule(function()
|
||||
if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then
|
||||
-- Always calculate 1/4 of current screen width
|
||||
local target_width = math.max(math.floor(vim.o.columns * 0.25), 30)
|
||||
state.target_width = target_width
|
||||
|
||||
local current_width = vim.api.nvim_win_get_width(state.output_win)
|
||||
if current_width ~= target_width then
|
||||
pcall(vim.api.nvim_win_set_width, state.output_win, target_width)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end,
|
||||
desc = "Maintain Ask panel at 1/4 window width",
|
||||
})
|
||||
end
|
||||
|
||||
--- Open the ask panel
|
||||
function M.open()
|
||||
-- Use the is_open() function which validates window state
|
||||
if M.is_open() then
|
||||
M.focus_input()
|
||||
return
|
||||
end
|
||||
|
||||
local dims = calculate_dimensions()
|
||||
|
||||
-- Store the target width
|
||||
state.target_width = dims.width
|
||||
|
||||
-- Create buffers if they don't exist
|
||||
if not state.output_buf or not vim.api.nvim_buf_is_valid(state.output_buf) then
|
||||
state.output_buf = create_output_buffer()
|
||||
setup_output_keymaps(state.output_buf)
|
||||
end
|
||||
|
||||
if not state.input_buf or not vim.api.nvim_buf_is_valid(state.input_buf) then
|
||||
state.input_buf = create_input_buffer()
|
||||
setup_input_keymaps(state.input_buf)
|
||||
end
|
||||
|
||||
-- Save current window to return to it later
|
||||
local current_win = vim.api.nvim_get_current_win()
|
||||
|
||||
-- Create output window (top-left)
|
||||
vim.cmd("topleft vsplit")
|
||||
state.output_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(state.output_win, state.output_buf)
|
||||
vim.api.nvim_win_set_width(state.output_win, dims.width)
|
||||
|
||||
-- Window options for output
|
||||
vim.wo[state.output_win].number = false
|
||||
vim.wo[state.output_win].relativenumber = false
|
||||
vim.wo[state.output_win].signcolumn = "no"
|
||||
vim.wo[state.output_win].wrap = true
|
||||
vim.wo[state.output_win].linebreak = true
|
||||
vim.wo[state.output_win].cursorline = false
|
||||
vim.wo[state.output_win].winfixwidth = true
|
||||
|
||||
-- Create input window (bottom of the left panel)
|
||||
vim.cmd("belowright split")
|
||||
state.input_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(state.input_win, state.input_buf)
|
||||
vim.api.nvim_win_set_height(state.input_win, dims.input_height)
|
||||
|
||||
-- Window options for input
|
||||
vim.wo[state.input_win].number = false
|
||||
vim.wo[state.input_win].relativenumber = false
|
||||
vim.wo[state.input_win].signcolumn = "no"
|
||||
vim.wo[state.input_win].wrap = true
|
||||
vim.wo[state.input_win].linebreak = true
|
||||
vim.wo[state.input_win].winfixheight = true
|
||||
vim.wo[state.input_win].winfixwidth = true
|
||||
|
||||
state.is_open = true
|
||||
|
||||
-- Setup autocmd to maintain width
|
||||
setup_width_autocmd()
|
||||
|
||||
-- Setup autocmd to close both windows when one is closed
|
||||
local close_group = vim.api.nvim_create_augroup("CodetypeAskClose", { clear = true })
|
||||
|
||||
vim.api.nvim_create_autocmd("WinClosed", {
|
||||
group = close_group,
|
||||
callback = function(args)
|
||||
local closed_win = tonumber(args.match)
|
||||
-- Check if one of our windows was closed
|
||||
if closed_win == state.output_win or closed_win == state.input_win then
|
||||
-- Defer to avoid issues during window close
|
||||
vim.schedule(function()
|
||||
-- Close the other window if it's still open
|
||||
if closed_win == state.output_win then
|
||||
if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then
|
||||
pcall(vim.api.nvim_win_close, state.input_win, true)
|
||||
end
|
||||
elseif closed_win == state.input_win then
|
||||
if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then
|
||||
pcall(vim.api.nvim_win_close, state.output_win, true)
|
||||
end
|
||||
end
|
||||
|
||||
-- Reset state
|
||||
state.input_win = nil
|
||||
state.output_win = nil
|
||||
state.is_open = false
|
||||
state.target_width = nil
|
||||
|
||||
-- Clean up autocmd groups
|
||||
pcall(vim.api.nvim_del_augroup_by_id, close_group)
|
||||
if ask_augroup then
|
||||
pcall(vim.api.nvim_del_augroup_by_id, ask_augroup)
|
||||
ask_augroup = nil
|
||||
end
|
||||
end)
|
||||
end
|
||||
end,
|
||||
desc = "Close both Ask windows together",
|
||||
})
|
||||
|
||||
-- Focus the input window and start insert mode
|
||||
vim.api.nvim_set_current_win(state.input_win)
|
||||
vim.cmd("startinsert")
|
||||
end
|
||||
|
||||
--- Show file picker for @ mentions
|
||||
function M.show_file_picker()
|
||||
-- Check if telescope is available
|
||||
local has_telescope, telescope = pcall(require, "telescope.builtin")
|
||||
|
||||
if has_telescope then
|
||||
telescope.find_files({
|
||||
prompt_title = "Select file to reference (@)",
|
||||
attach_mappings = function(prompt_bufnr, map)
|
||||
local actions = require("telescope.actions")
|
||||
local action_state = require("telescope.actions.state")
|
||||
|
||||
actions.select_default:replace(function()
|
||||
actions.close(prompt_bufnr)
|
||||
local selection = action_state.get_selected_entry()
|
||||
if selection then
|
||||
local filepath = selection.path or selection[1]
|
||||
local filename = vim.fn.fnamemodify(filepath, ":t")
|
||||
M.add_file_reference(filepath, filename)
|
||||
end
|
||||
end)
|
||||
return true
|
||||
end,
|
||||
})
|
||||
else
|
||||
-- Fallback: simple input
|
||||
vim.ui.input({ prompt = "Enter file path: " }, function(input)
|
||||
if input and input ~= "" then
|
||||
local filepath = vim.fn.fnamemodify(input, ":p")
|
||||
local filename = vim.fn.fnamemodify(filepath, ":t")
|
||||
M.add_file_reference(filepath, filename)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
--- Add a file reference to the input
|
||||
---@param filepath string Full path to the file
|
||||
---@param filename string Display name
|
||||
function M.add_file_reference(filepath, filename)
|
||||
-- Normalize filepath
|
||||
filepath = vim.fn.fnamemodify(filepath, ":p")
|
||||
|
||||
-- Store the reference with full path
|
||||
state.referenced_files[filename] = filepath
|
||||
|
||||
-- Read and validate file exists
|
||||
local content = utils.read_file(filepath)
|
||||
if not content then
|
||||
utils.notify("Warning: Could not read file: " .. filename, vim.log.levels.WARN)
|
||||
end
|
||||
|
||||
-- Add to input buffer
|
||||
if state.input_buf and vim.api.nvim_buf_is_valid(state.input_buf) then
|
||||
local lines = vim.api.nvim_buf_get_lines(state.input_buf, 0, -1, false)
|
||||
local text = table.concat(lines, "\n")
|
||||
|
||||
-- Clear placeholder if present
|
||||
if text:match("Type your question here") then
|
||||
text = ""
|
||||
end
|
||||
|
||||
-- Add file reference (with single @)
|
||||
local reference = "[📎 " .. filename .. "] "
|
||||
text = text .. reference
|
||||
|
||||
vim.api.nvim_buf_set_lines(state.input_buf, 0, -1, false, vim.split(text, "\n"))
|
||||
|
||||
-- Move cursor to end
|
||||
if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then
|
||||
vim.api.nvim_set_current_win(state.input_win)
|
||||
local line_count = vim.api.nvim_buf_line_count(state.input_buf)
|
||||
local last_line = vim.api.nvim_buf_get_lines(state.input_buf, line_count - 1, line_count, false)[1] or ""
|
||||
vim.api.nvim_win_set_cursor(state.input_win, { line_count, #last_line })
|
||||
vim.cmd("startinsert!")
|
||||
end
|
||||
end
|
||||
|
||||
utils.notify("Added file: " .. filename .. " (" .. (content and #content or 0) .. " bytes)")
|
||||
end
|
||||
|
||||
--- Close the ask panel
|
||||
function M.close()
|
||||
-- Remove the width maintenance autocmd first
|
||||
if ask_augroup then
|
||||
pcall(vim.api.nvim_del_augroup_by_id, ask_augroup)
|
||||
ask_augroup = nil
|
||||
end
|
||||
|
||||
-- Find a window to focus after closing (not the ask windows)
|
||||
local target_win = nil
|
||||
for _, win in ipairs(vim.api.nvim_list_wins()) do
|
||||
local buf = vim.api.nvim_win_get_buf(win)
|
||||
if win ~= state.input_win and win ~= state.output_win then
|
||||
local buftype = vim.bo[buf].buftype
|
||||
if buftype == "" or buftype == "acwrite" then
|
||||
target_win = win
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Close input window
|
||||
if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then
|
||||
pcall(vim.api.nvim_win_close, state.input_win, true)
|
||||
end
|
||||
|
||||
-- Close output window
|
||||
if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then
|
||||
pcall(vim.api.nvim_win_close, state.output_win, true)
|
||||
end
|
||||
|
||||
-- Reset state
|
||||
state.input_win = nil
|
||||
state.output_win = nil
|
||||
state.is_open = false
|
||||
state.target_width = nil
|
||||
|
||||
-- Focus the target window if found, otherwise focus first available
|
||||
if target_win and vim.api.nvim_win_is_valid(target_win) then
|
||||
pcall(vim.api.nvim_set_current_win, target_win)
|
||||
else
|
||||
-- If no valid window, make sure we're not left with empty state
|
||||
local wins = vim.api.nvim_list_wins()
|
||||
if #wins > 0 then
|
||||
pcall(vim.api.nvim_set_current_win, wins[1])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Toggle the ask panel
|
||||
function M.toggle()
|
||||
if M.is_open() then
|
||||
M.close()
|
||||
else
|
||||
M.open()
|
||||
end
|
||||
end
|
||||
|
||||
--- Focus the input window
|
||||
function M.focus_input()
|
||||
if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then
|
||||
vim.api.nvim_set_current_win(state.input_win)
|
||||
vim.cmd("startinsert")
|
||||
end
|
||||
end
|
||||
|
||||
--- Focus the output window
|
||||
function M.focus_output()
|
||||
if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then
|
||||
vim.api.nvim_set_current_win(state.output_win)
|
||||
end
|
||||
end
|
||||
|
||||
--- Get input text
|
||||
---@return string Input text
|
||||
local function get_input_text()
|
||||
if not state.input_buf or not vim.api.nvim_buf_is_valid(state.input_buf) then
|
||||
return ""
|
||||
end
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(state.input_buf, 0, -1, false)
|
||||
local content = table.concat(lines, "\n")
|
||||
|
||||
-- Ignore placeholder
|
||||
if content:match("Type your question here") then
|
||||
return ""
|
||||
end
|
||||
|
||||
return content
|
||||
end
|
||||
|
||||
--- Clear input buffer
|
||||
function M.clear_input()
|
||||
if state.input_buf and vim.api.nvim_buf_is_valid(state.input_buf) then
|
||||
vim.api.nvim_buf_set_lines(state.input_buf, 0, -1, false, { "" })
|
||||
end
|
||||
state.referenced_files = {}
|
||||
end
|
||||
|
||||
--- Append text to output buffer
|
||||
---@param text string Text to append
|
||||
---@param is_user boolean Whether this is user message
|
||||
local function append_to_output(text, is_user)
|
||||
if not state.output_buf or not vim.api.nvim_buf_is_valid(state.output_buf) then
|
||||
return
|
||||
end
|
||||
|
||||
vim.bo[state.output_buf].modifiable = true
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(state.output_buf, 0, -1, false)
|
||||
|
||||
local timestamp = os.date("%H:%M")
|
||||
local header = is_user
|
||||
and "┌─ 👤 You [" .. timestamp .. "] ────────"
|
||||
or "┌─ 🤖 AI [" .. timestamp .. "] ──────────"
|
||||
|
||||
local new_lines = { "", header, "│" }
|
||||
|
||||
-- Add text lines with border
|
||||
for _, line in ipairs(vim.split(text, "\n")) do
|
||||
table.insert(new_lines, "│ " .. line)
|
||||
end
|
||||
|
||||
table.insert(new_lines, "└─────────────────────────────────")
|
||||
|
||||
for _, line in ipairs(new_lines) do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_lines(state.output_buf, 0, -1, false, lines)
|
||||
vim.bo[state.output_buf].modifiable = false
|
||||
|
||||
-- Scroll to bottom
|
||||
if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then
|
||||
local line_count = vim.api.nvim_buf_line_count(state.output_buf)
|
||||
vim.api.nvim_win_set_cursor(state.output_win, { line_count, 0 })
|
||||
end
|
||||
end
|
||||
|
||||
--- Build context from referenced files
|
||||
---@return string Context string, number File count
|
||||
local function build_file_context()
|
||||
local context = ""
|
||||
local file_count = 0
|
||||
|
||||
for filename, filepath in pairs(state.referenced_files) do
|
||||
local content = utils.read_file(filepath)
|
||||
if content and content ~= "" then
|
||||
-- Detect language from extension
|
||||
local ext = vim.fn.fnamemodify(filepath, ":e")
|
||||
local lang = ext or "text"
|
||||
|
||||
context = context .. "\n\n=== FILE: " .. filename .. " ===\n"
|
||||
context = context .. "Path: " .. filepath .. "\n"
|
||||
context = context .. "```" .. lang .. "\n" .. content .. "\n```\n"
|
||||
file_count = file_count + 1
|
||||
end
|
||||
end
|
||||
|
||||
return context, file_count
|
||||
end
|
||||
|
||||
--- Build context for the question
|
||||
---@return table Context object
|
||||
local function build_context()
|
||||
local context = {
|
||||
project_root = utils.get_project_root(),
|
||||
current_file = nil,
|
||||
current_content = nil,
|
||||
language = nil,
|
||||
referenced_files = state.referenced_files,
|
||||
}
|
||||
|
||||
-- Try to get current file context from the non-ask window
|
||||
local wins = vim.api.nvim_list_wins()
|
||||
for _, win in ipairs(wins) do
|
||||
if win ~= state.input_win and win ~= state.output_win then
|
||||
local buf = vim.api.nvim_win_get_buf(win)
|
||||
local filepath = vim.api.nvim_buf_get_name(buf)
|
||||
|
||||
if filepath and filepath ~= "" and not filepath:match("%.coder%.") then
|
||||
context.current_file = filepath
|
||||
context.current_content = table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, false), "\n")
|
||||
context.language = vim.bo[buf].filetype
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return context
|
||||
end
|
||||
|
||||
--- Submit the question to LLM
|
||||
function M.submit()
|
||||
local question = get_input_text()
|
||||
|
||||
if not question or question:match("^%s*$") then
|
||||
utils.notify("Please enter a question", vim.log.levels.WARN)
|
||||
M.focus_input()
|
||||
return
|
||||
end
|
||||
|
||||
-- Build context BEFORE clearing input (to preserve file references)
|
||||
local context = build_context()
|
||||
local file_context, file_count = build_file_context()
|
||||
|
||||
-- Build display message (without full file contents)
|
||||
local display_question = question
|
||||
if file_count > 0 then
|
||||
display_question = question .. "\n📎 " .. file_count .. " file(s) attached"
|
||||
end
|
||||
|
||||
-- Add user message to output
|
||||
append_to_output(display_question, true)
|
||||
|
||||
-- Clear input and references AFTER building context
|
||||
M.clear_input()
|
||||
|
||||
-- Build system prompt for ask mode using prompts module
|
||||
local prompts = require("codetyper.prompts")
|
||||
local system_prompt = prompts.system.ask
|
||||
|
||||
if context.current_file then
|
||||
system_prompt = system_prompt .. "\n\nCurrent open file: " .. context.current_file
|
||||
system_prompt = system_prompt .. "\nLanguage: " .. (context.language or "unknown")
|
||||
end
|
||||
|
||||
-- Add to history
|
||||
table.insert(state.history, { role = "user", content = question })
|
||||
|
||||
-- Show loading indicator
|
||||
append_to_output("⏳ Thinking...", false)
|
||||
|
||||
-- Get LLM client and generate response
|
||||
local ok, llm = pcall(require, "codetyper.llm")
|
||||
if not ok then
|
||||
append_to_output("❌ Error: LLM module not loaded", false)
|
||||
return
|
||||
end
|
||||
|
||||
local client = llm.get_client()
|
||||
|
||||
-- Build full prompt WITH file contents
|
||||
local full_prompt = question
|
||||
if file_context ~= "" then
|
||||
full_prompt = "USER QUESTION: " .. question .. "\n\n" ..
|
||||
"ATTACHED FILE CONTENTS (please analyze these):" .. file_context
|
||||
end
|
||||
|
||||
-- Also add current file if no files were explicitly attached
|
||||
if file_count == 0 and context.current_content and context.current_content ~= "" then
|
||||
full_prompt = "USER QUESTION: " .. question .. "\n\n" ..
|
||||
"CURRENT FILE (" .. (context.current_file or "unknown") .. "):\n```\n" ..
|
||||
context.current_content .. "\n```"
|
||||
end
|
||||
|
||||
local request_context = {
|
||||
file_content = file_context ~= "" and file_context or context.current_content,
|
||||
language = context.language,
|
||||
prompt_type = "explain",
|
||||
file_path = context.current_file,
|
||||
}
|
||||
|
||||
client.generate(full_prompt, request_context, function(response, err)
|
||||
-- Remove loading indicator
|
||||
if state.output_buf and vim.api.nvim_buf_is_valid(state.output_buf) then
|
||||
vim.bo[state.output_buf].modifiable = true
|
||||
local lines = vim.api.nvim_buf_get_lines(state.output_buf, 0, -1, false)
|
||||
-- Remove last few lines (the thinking message)
|
||||
local to_remove = 0
|
||||
for i = #lines, 1, -1 do
|
||||
if lines[i]:match("Thinking") or lines[i]:match("^[│└┌─]") then
|
||||
to_remove = to_remove + 1
|
||||
if lines[i]:match("┌") then
|
||||
break
|
||||
end
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
for _ = 1, math.min(to_remove, 5) do
|
||||
table.remove(lines)
|
||||
end
|
||||
vim.api.nvim_buf_set_lines(state.output_buf, 0, -1, false, lines)
|
||||
vim.bo[state.output_buf].modifiable = false
|
||||
end
|
||||
|
||||
if err then
|
||||
append_to_output("❌ Error: " .. err, false)
|
||||
return
|
||||
end
|
||||
|
||||
if response then
|
||||
-- Add to history
|
||||
table.insert(state.history, { role = "assistant", content = response })
|
||||
-- Display response
|
||||
append_to_output(response, false)
|
||||
else
|
||||
append_to_output("❌ No response received", false)
|
||||
end
|
||||
|
||||
-- Focus back to input
|
||||
M.focus_input()
|
||||
end)
|
||||
end
|
||||
|
||||
--- Clear chat history
|
||||
function M.clear_history()
|
||||
state.history = {}
|
||||
state.referenced_files = {}
|
||||
|
||||
if state.output_buf and vim.api.nvim_buf_is_valid(state.output_buf) then
|
||||
local header = {
|
||||
"╔═══════════════════════════════════╗",
|
||||
"║ 🤖 CODETYPER ASK ║",
|
||||
"╠═══════════════════════════════════╣",
|
||||
"║ Ask about code or concepts ║",
|
||||
"║ ║",
|
||||
"║ 💡 Keymaps: ║",
|
||||
"║ @ → attach file ║",
|
||||
"║ C-Enter → send ║",
|
||||
"║ C-n → new chat ║",
|
||||
"║ C-f → add current file ║",
|
||||
"║ C-h/j/k/l → navigate ║",
|
||||
"║ q → close │ K/J → jump ║",
|
||||
"╚═══════════════════════════════════╝",
|
||||
"",
|
||||
}
|
||||
vim.bo[state.output_buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(state.output_buf, 0, -1, false, header)
|
||||
vim.bo[state.output_buf].modifiable = false
|
||||
end
|
||||
|
||||
utils.notify("Chat history cleared")
|
||||
end
|
||||
|
||||
--- Start a new chat (clears history and input)
|
||||
function M.new_chat()
|
||||
-- Clear the input
|
||||
M.clear_input()
|
||||
-- Clear the history
|
||||
M.clear_history()
|
||||
-- Focus the input
|
||||
M.focus_input()
|
||||
utils.notify("Started new chat")
|
||||
end
|
||||
|
||||
--- Include current file context in input
|
||||
function M.include_file_context()
|
||||
local context = build_context()
|
||||
|
||||
if not context.current_file then
|
||||
utils.notify("No file context available", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
local filename = vim.fn.fnamemodify(context.current_file, ":t")
|
||||
M.add_file_reference(context.current_file, filename)
|
||||
end
|
||||
|
||||
--- Copy last assistant response to clipboard
|
||||
function M.copy_last_response()
|
||||
for i = #state.history, 1, -1 do
|
||||
if state.history[i].role == "assistant" then
|
||||
vim.fn.setreg("+", state.history[i].content)
|
||||
utils.notify("Response copied to clipboard")
|
||||
return
|
||||
end
|
||||
end
|
||||
utils.notify("No response to copy", vim.log.levels.WARN)
|
||||
end
|
||||
--- Check if ask panel is open (validates window state)
|
||||
---@return boolean
|
||||
function M.is_open()
|
||||
-- Verify windows are actually valid, not just the flag
|
||||
if state.is_open then
|
||||
local output_valid = state.output_win and vim.api.nvim_win_is_valid(state.output_win)
|
||||
local input_valid = state.input_win and vim.api.nvim_win_is_valid(state.input_win)
|
||||
|
||||
-- If either window is invalid, reset the state
|
||||
if not output_valid or not input_valid then
|
||||
state.is_open = false
|
||||
state.output_win = nil
|
||||
state.input_win = nil
|
||||
state.target_width = nil
|
||||
-- Clean up autocmd
|
||||
if ask_augroup then
|
||||
pcall(vim.api.nvim_del_augroup_by_id, ask_augroup)
|
||||
ask_augroup = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return state.is_open
|
||||
end
|
||||
|
||||
--- Get chat history
|
||||
---@return table History
|
||||
function M.get_history()
|
||||
return state.history
|
||||
end
|
||||
|
||||
return M
|
||||
349
lua/codetyper/autocmds.lua
Normal file
349
lua/codetyper/autocmds.lua
Normal file
@@ -0,0 +1,349 @@
|
||||
---@mod codetyper.autocmds Autocommands for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
|
||||
--- Autocommand group name
|
||||
local AUGROUP = "Codetyper"
|
||||
|
||||
--- Debounce timer for tree updates
|
||||
local tree_update_timer = nil
|
||||
local TREE_UPDATE_DEBOUNCE_MS = 1000 -- 1 second debounce
|
||||
|
||||
--- Track processed prompts to avoid re-processing
|
||||
---@type table<string, boolean>
|
||||
local processed_prompts = {}
|
||||
|
||||
--- Generate a unique key for a prompt
|
||||
---@param bufnr number Buffer number
|
||||
---@param prompt table Prompt object
|
||||
---@return string Unique key
|
||||
local function get_prompt_key(bufnr, prompt)
|
||||
return string.format("%d:%d:%d:%s", bufnr, prompt.start_line, prompt.end_line, prompt.content:sub(1, 50))
|
||||
end
|
||||
|
||||
--- Schedule tree update with debounce
|
||||
local function schedule_tree_update()
|
||||
if tree_update_timer then
|
||||
tree_update_timer:stop()
|
||||
end
|
||||
|
||||
tree_update_timer = vim.defer_fn(function()
|
||||
local tree = require("codetyper.tree")
|
||||
tree.update_tree_log()
|
||||
tree_update_timer = nil
|
||||
end, TREE_UPDATE_DEBOUNCE_MS)
|
||||
end
|
||||
|
||||
--- Setup autocommands
|
||||
function M.setup()
|
||||
local group = vim.api.nvim_create_augroup(AUGROUP, { clear = true })
|
||||
|
||||
-- Auto-save coder file when leaving insert mode
|
||||
vim.api.nvim_create_autocmd("InsertLeave", {
|
||||
group = group,
|
||||
pattern = "*.coder.*",
|
||||
callback = function()
|
||||
-- Auto-save the coder file
|
||||
if vim.bo.modified then
|
||||
vim.cmd("silent! write")
|
||||
end
|
||||
-- Check for closed prompts and auto-process
|
||||
M.check_for_closed_prompt()
|
||||
end,
|
||||
desc = "Auto-save and check for closed prompt tags",
|
||||
})
|
||||
|
||||
-- Auto-set filetype for coder files based on extension
|
||||
vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, {
|
||||
group = group,
|
||||
pattern = "*.coder.*",
|
||||
callback = function()
|
||||
M.set_coder_filetype()
|
||||
end,
|
||||
desc = "Set filetype for coder files",
|
||||
})
|
||||
|
||||
-- Auto-open split view when opening a coder file directly (e.g., from nvim-tree)
|
||||
vim.api.nvim_create_autocmd("BufEnter", {
|
||||
group = group,
|
||||
pattern = "*.coder.*",
|
||||
callback = function()
|
||||
-- Delay slightly to ensure buffer is fully loaded
|
||||
vim.defer_fn(function()
|
||||
M.auto_open_target_file()
|
||||
end, 50)
|
||||
end,
|
||||
desc = "Auto-open target file when coder file is opened",
|
||||
})
|
||||
|
||||
-- Cleanup on buffer close
|
||||
vim.api.nvim_create_autocmd("BufWipeout", {
|
||||
group = group,
|
||||
pattern = "*.coder.*",
|
||||
callback = function(ev)
|
||||
local window = require("codetyper.window")
|
||||
if window.is_open() then
|
||||
window.close_split()
|
||||
end
|
||||
-- Clear processed prompts for this buffer
|
||||
local bufnr = ev.buf
|
||||
for key, _ in pairs(processed_prompts) do
|
||||
if key:match("^" .. bufnr .. ":") then
|
||||
processed_prompts[key] = nil
|
||||
end
|
||||
end
|
||||
-- Clear auto-opened tracking
|
||||
M.clear_auto_opened(bufnr)
|
||||
end,
|
||||
desc = "Cleanup on coder buffer close",
|
||||
})
|
||||
|
||||
-- Update tree.log when files are created/written
|
||||
vim.api.nvim_create_autocmd({ "BufWritePost", "BufNewFile" }, {
|
||||
group = group,
|
||||
pattern = "*",
|
||||
callback = function(ev)
|
||||
-- Skip coder files and tree.log itself
|
||||
local filepath = ev.file or vim.fn.expand("%:p")
|
||||
if filepath:match("%.coder%.") or filepath:match("tree%.log$") then
|
||||
return
|
||||
end
|
||||
-- Schedule tree update with debounce
|
||||
schedule_tree_update()
|
||||
end,
|
||||
desc = "Update tree.log on file creation/save",
|
||||
})
|
||||
|
||||
-- Update tree.log when files are deleted (via netrw or file explorer)
|
||||
vim.api.nvim_create_autocmd("BufDelete", {
|
||||
group = group,
|
||||
pattern = "*",
|
||||
callback = function(ev)
|
||||
local filepath = ev.file or ""
|
||||
-- Skip special buffers and coder files
|
||||
if filepath == "" or filepath:match("%.coder%.") or filepath:match("tree%.log$") then
|
||||
return
|
||||
end
|
||||
schedule_tree_update()
|
||||
end,
|
||||
desc = "Update tree.log on file deletion",
|
||||
})
|
||||
|
||||
-- Update tree on directory change
|
||||
vim.api.nvim_create_autocmd("DirChanged", {
|
||||
group = group,
|
||||
pattern = "*",
|
||||
callback = function()
|
||||
schedule_tree_update()
|
||||
end,
|
||||
desc = "Update tree.log on directory change",
|
||||
})
|
||||
end
|
||||
|
||||
--- Check if the buffer has a newly closed prompt and auto-process
|
||||
function M.check_for_closed_prompt()
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
local parser = require("codetyper.parser")
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
|
||||
-- Get current line
|
||||
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||
local line = cursor[1]
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, line - 1, line, false)
|
||||
|
||||
if #lines == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local current_line = lines[1]
|
||||
|
||||
-- Check if line contains closing tag
|
||||
if parser.has_closing_tag(current_line, config.patterns.close_tag) then
|
||||
-- Find the complete prompt
|
||||
local prompt = parser.get_last_prompt(bufnr)
|
||||
if prompt and prompt.content and prompt.content ~= "" then
|
||||
-- Generate unique key for this prompt
|
||||
local prompt_key = get_prompt_key(bufnr, prompt)
|
||||
|
||||
-- Check if already processed
|
||||
if processed_prompts[prompt_key] then
|
||||
return
|
||||
end
|
||||
|
||||
-- Mark as processed
|
||||
processed_prompts[prompt_key] = true
|
||||
|
||||
-- Auto-process the prompt (no confirmation needed)
|
||||
utils.notify("Processing prompt...", vim.log.levels.INFO)
|
||||
vim.schedule(function()
|
||||
vim.cmd("CoderProcess")
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Reset processed prompts for a buffer (useful for re-processing)
|
||||
---@param bufnr? number Buffer number (default: current)
|
||||
function M.reset_processed(bufnr)
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
for key, _ in pairs(processed_prompts) do
|
||||
if key:match("^" .. bufnr .. ":") then
|
||||
processed_prompts[key] = nil
|
||||
end
|
||||
end
|
||||
utils.notify("Prompt history cleared - prompts can be re-processed")
|
||||
end
|
||||
|
||||
--- Track if we already opened the split for this buffer
|
||||
---@type table<number, boolean>
|
||||
local auto_opened_buffers = {}
|
||||
|
||||
--- Auto-open target file when a coder file is opened directly
|
||||
function M.auto_open_target_file()
|
||||
local window = require("codetyper.window")
|
||||
|
||||
-- Skip if split is already open
|
||||
if window.is_open() then
|
||||
return
|
||||
end
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
|
||||
-- Skip if we already handled this buffer
|
||||
if auto_opened_buffers[bufnr] then
|
||||
return
|
||||
end
|
||||
|
||||
local current_file = vim.fn.expand("%:p")
|
||||
|
||||
-- Skip empty paths
|
||||
if not current_file or current_file == "" then
|
||||
return
|
||||
end
|
||||
|
||||
-- Verify it's a coder file
|
||||
if not utils.is_coder_file(current_file) then
|
||||
return
|
||||
end
|
||||
|
||||
-- Skip if we're in a special buffer (nvim-tree, etc.)
|
||||
local buftype = vim.bo[bufnr].buftype
|
||||
if buftype ~= "" then
|
||||
return
|
||||
end
|
||||
|
||||
-- Mark as handled
|
||||
auto_opened_buffers[bufnr] = true
|
||||
|
||||
-- Get the target file path
|
||||
local target_path = utils.get_target_path(current_file)
|
||||
|
||||
-- Check if target file exists
|
||||
if not utils.file_exists(target_path) then
|
||||
utils.notify("Target file not found: " .. vim.fn.fnamemodify(target_path, ":t"), vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
-- Get config with fallback defaults
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
-- Fallback width if config not fully loaded
|
||||
local width = (config and config.window and config.window.width) or 0.4
|
||||
if width <= 1 then
|
||||
width = math.floor(vim.o.columns * width)
|
||||
end
|
||||
|
||||
-- Store current coder window
|
||||
local coder_win = vim.api.nvim_get_current_win()
|
||||
local coder_buf = bufnr
|
||||
|
||||
-- Open target file in a vertical split on the right
|
||||
local ok, err = pcall(function()
|
||||
vim.cmd("vsplit " .. vim.fn.fnameescape(target_path))
|
||||
end)
|
||||
|
||||
if not ok then
|
||||
utils.notify("Failed to open target file: " .. tostring(err), vim.log.levels.ERROR)
|
||||
auto_opened_buffers[bufnr] = nil -- Allow retry
|
||||
return
|
||||
end
|
||||
|
||||
-- Now we're in the target window (right side)
|
||||
local target_win = vim.api.nvim_get_current_win()
|
||||
local target_buf = vim.api.nvim_get_current_buf()
|
||||
|
||||
-- Set the coder window width (left side)
|
||||
pcall(vim.api.nvim_win_set_width, coder_win, width)
|
||||
|
||||
-- Update window module state
|
||||
window._coder_win = coder_win
|
||||
window._coder_buf = coder_buf
|
||||
window._target_win = target_win
|
||||
window._target_buf = target_buf
|
||||
|
||||
-- Set up window options for coder window
|
||||
pcall(function()
|
||||
vim.wo[coder_win].number = true
|
||||
vim.wo[coder_win].relativenumber = true
|
||||
vim.wo[coder_win].signcolumn = "yes"
|
||||
end)
|
||||
|
||||
utils.notify("Opened target: " .. vim.fn.fnamemodify(target_path, ":t"))
|
||||
end
|
||||
|
||||
--- Clear auto-opened tracking for a buffer
|
||||
---@param bufnr number Buffer number
|
||||
function M.clear_auto_opened(bufnr)
|
||||
auto_opened_buffers[bufnr] = nil
|
||||
end
|
||||
|
||||
--- Set appropriate filetype for coder files
|
||||
function M.set_coder_filetype()
|
||||
local filepath = vim.fn.expand("%:p")
|
||||
|
||||
-- Extract the actual extension (e.g., index.coder.ts -> ts)
|
||||
local ext = filepath:match("%.coder%.(%w+)$")
|
||||
|
||||
if ext then
|
||||
-- Map extension to filetype
|
||||
local ft_map = {
|
||||
ts = "typescript",
|
||||
tsx = "typescriptreact",
|
||||
js = "javascript",
|
||||
jsx = "javascriptreact",
|
||||
py = "python",
|
||||
lua = "lua",
|
||||
go = "go",
|
||||
rs = "rust",
|
||||
rb = "ruby",
|
||||
java = "java",
|
||||
c = "c",
|
||||
cpp = "cpp",
|
||||
cs = "cs",
|
||||
json = "json",
|
||||
yaml = "yaml",
|
||||
yml = "yaml",
|
||||
md = "markdown",
|
||||
html = "html",
|
||||
css = "css",
|
||||
scss = "scss",
|
||||
vue = "vue",
|
||||
svelte = "svelte",
|
||||
}
|
||||
|
||||
local filetype = ft_map[ext] or ext
|
||||
vim.bo.filetype = filetype
|
||||
end
|
||||
end
|
||||
|
||||
--- Clear all autocommands
|
||||
function M.clear()
|
||||
vim.api.nvim_del_augroup_by_name(AUGROUP)
|
||||
end
|
||||
|
||||
return M
|
||||
330
lua/codetyper/commands.lua
Normal file
330
lua/codetyper/commands.lua
Normal file
@@ -0,0 +1,330 @@
|
||||
---@mod codetyper.commands Command definitions for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local window = require("codetyper.window")
|
||||
|
||||
--- Open coder view for current file or select one
|
||||
---@param opts? table Command options
|
||||
local function cmd_open(opts)
|
||||
opts = opts or {}
|
||||
|
||||
local current_file = vim.fn.expand("%:p")
|
||||
|
||||
-- If no file is open, prompt user to select one
|
||||
if current_file == "" or vim.bo.buftype ~= "" then
|
||||
-- Use telescope or vim.ui.select to pick a file
|
||||
if pcall(require, "telescope") then
|
||||
require("telescope.builtin").find_files({
|
||||
prompt_title = "Select file for Coder",
|
||||
attach_mappings = function(prompt_bufnr, map)
|
||||
local actions = require("telescope.actions")
|
||||
local action_state = require("telescope.actions.state")
|
||||
|
||||
actions.select_default:replace(function()
|
||||
actions.close(prompt_bufnr)
|
||||
local selection = action_state.get_selected_entry()
|
||||
if selection then
|
||||
local target_path = selection.path or selection[1]
|
||||
local coder_path = utils.get_coder_path(target_path)
|
||||
window.open_split(target_path, coder_path)
|
||||
end
|
||||
end)
|
||||
return true
|
||||
end,
|
||||
})
|
||||
else
|
||||
-- Fallback to input prompt
|
||||
vim.ui.input({ prompt = "Enter file path: " }, function(input)
|
||||
if input and input ~= "" then
|
||||
local target_path = vim.fn.fnamemodify(input, ":p")
|
||||
local coder_path = utils.get_coder_path(target_path)
|
||||
window.open_split(target_path, coder_path)
|
||||
end
|
||||
end)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local target_path, coder_path
|
||||
|
||||
-- Check if current file is a coder file
|
||||
if utils.is_coder_file(current_file) then
|
||||
coder_path = current_file
|
||||
target_path = utils.get_target_path(current_file)
|
||||
else
|
||||
target_path = current_file
|
||||
coder_path = utils.get_coder_path(current_file)
|
||||
end
|
||||
|
||||
window.open_split(target_path, coder_path)
|
||||
end
|
||||
|
||||
--- Close coder view
|
||||
local function cmd_close()
|
||||
window.close_split()
|
||||
end
|
||||
|
||||
--- Toggle coder view
|
||||
local function cmd_toggle()
|
||||
local current_file = vim.fn.expand("%:p")
|
||||
|
||||
if current_file == "" then
|
||||
utils.notify("No file in current buffer", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
local target_path, coder_path
|
||||
|
||||
if utils.is_coder_file(current_file) then
|
||||
coder_path = current_file
|
||||
target_path = utils.get_target_path(current_file)
|
||||
else
|
||||
target_path = current_file
|
||||
coder_path = utils.get_coder_path(current_file)
|
||||
end
|
||||
|
||||
window.toggle_split(target_path, coder_path)
|
||||
end
|
||||
|
||||
--- Process prompt at cursor and generate code
|
||||
local function cmd_process()
|
||||
local parser = require("codetyper.parser")
|
||||
local llm = require("codetyper.llm")
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local current_file = vim.fn.expand("%:p")
|
||||
|
||||
if not utils.is_coder_file(current_file) then
|
||||
utils.notify("Not a coder file. Use *.coder.* files", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
local prompt = parser.get_last_prompt(bufnr)
|
||||
if not prompt then
|
||||
utils.notify("No prompt found. Use /@ your prompt @/", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
local target_path = utils.get_target_path(current_file)
|
||||
local prompt_type = parser.detect_prompt_type(prompt.content)
|
||||
local context = llm.build_context(target_path, prompt_type)
|
||||
local clean_prompt = parser.clean_prompt(prompt.content)
|
||||
|
||||
llm.generate(clean_prompt, context, function(response, err)
|
||||
if err then
|
||||
utils.notify("Generation failed: " .. err, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
if response then
|
||||
-- Inject code into target file
|
||||
local inject = require("codetyper.inject")
|
||||
inject.inject_code(target_path, response, prompt_type)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Show plugin status
|
||||
local function cmd_status()
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
local tree = require("codetyper.tree")
|
||||
|
||||
local stats = tree.get_stats()
|
||||
|
||||
local status = {
|
||||
"Codetyper.nvim Status",
|
||||
"====================",
|
||||
"",
|
||||
"Provider: " .. config.llm.provider,
|
||||
}
|
||||
|
||||
if config.llm.provider == "claude" then
|
||||
local has_key = (config.llm.claude.api_key or vim.env.ANTHROPIC_API_KEY) ~= nil
|
||||
table.insert(status, "Claude API Key: " .. (has_key and "configured" or "NOT SET"))
|
||||
table.insert(status, "Claude Model: " .. config.llm.claude.model)
|
||||
else
|
||||
table.insert(status, "Ollama Host: " .. config.llm.ollama.host)
|
||||
table.insert(status, "Ollama Model: " .. config.llm.ollama.model)
|
||||
end
|
||||
|
||||
table.insert(status, "")
|
||||
table.insert(status, "Window Position: " .. config.window.position)
|
||||
table.insert(status, "Window Width: " .. tostring(config.window.width * 100) .. "%")
|
||||
table.insert(status, "")
|
||||
table.insert(status, "View Open: " .. (window.is_open() and "yes" or "no"))
|
||||
table.insert(status, "")
|
||||
table.insert(status, "Project Stats:")
|
||||
table.insert(status, " Files: " .. stats.files)
|
||||
table.insert(status, " Directories: " .. stats.directories)
|
||||
table.insert(status, " Tree Log: " .. (tree.get_tree_log_path() or "N/A"))
|
||||
|
||||
utils.notify(table.concat(status, "\n"))
|
||||
end
|
||||
|
||||
--- Refresh tree.log manually
|
||||
local function cmd_tree()
|
||||
local tree = require("codetyper.tree")
|
||||
if tree.update_tree_log() then
|
||||
utils.notify("Tree log updated: " .. tree.get_tree_log_path())
|
||||
else
|
||||
utils.notify("Failed to update tree log", vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
|
||||
--- Open tree.log file
|
||||
local function cmd_tree_view()
|
||||
local tree = require("codetyper.tree")
|
||||
local tree_log_path = tree.get_tree_log_path()
|
||||
|
||||
if not tree_log_path then
|
||||
utils.notify("Could not find tree.log", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
-- Ensure tree is up to date
|
||||
tree.update_tree_log()
|
||||
|
||||
-- Open in a new split
|
||||
vim.cmd("vsplit " .. vim.fn.fnameescape(tree_log_path))
|
||||
vim.bo.readonly = true
|
||||
vim.bo.modifiable = false
|
||||
end
|
||||
|
||||
--- Reset processed prompts to allow re-processing
|
||||
local function cmd_reset()
|
||||
local autocmds = require("codetyper.autocmds")
|
||||
autocmds.reset_processed()
|
||||
end
|
||||
|
||||
--- Force update gitignore
|
||||
local function cmd_gitignore()
|
||||
local gitignore = require("codetyper.gitignore")
|
||||
gitignore.force_update()
|
||||
end
|
||||
|
||||
--- Open ask panel
|
||||
local function cmd_ask()
|
||||
local ask = require("codetyper.ask")
|
||||
ask.open()
|
||||
end
|
||||
|
||||
--- Close ask panel
|
||||
local function cmd_ask_close()
|
||||
local ask = require("codetyper.ask")
|
||||
ask.close()
|
||||
end
|
||||
|
||||
--- Toggle ask panel
|
||||
local function cmd_ask_toggle()
|
||||
local ask = require("codetyper.ask")
|
||||
ask.toggle()
|
||||
end
|
||||
|
||||
--- Clear ask history
|
||||
local function cmd_ask_clear()
|
||||
local ask = require("codetyper.ask")
|
||||
ask.clear_history()
|
||||
end
|
||||
|
||||
--- Switch focus between coder and target windows
|
||||
local function cmd_focus()
|
||||
if not window.is_open() then
|
||||
utils.notify("Coder view not open", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
local current_win = vim.api.nvim_get_current_win()
|
||||
if current_win == window.get_coder_win() then
|
||||
window.focus_target()
|
||||
else
|
||||
window.focus_coder()
|
||||
end
|
||||
end
|
||||
|
||||
--- Main command dispatcher
|
||||
---@param args table Command arguments
|
||||
local function coder_cmd(args)
|
||||
local subcommand = args.fargs[1] or "toggle"
|
||||
|
||||
local commands = {
|
||||
open = cmd_open,
|
||||
close = cmd_close,
|
||||
toggle = cmd_toggle,
|
||||
process = cmd_process,
|
||||
status = cmd_status,
|
||||
focus = cmd_focus,
|
||||
tree = cmd_tree,
|
||||
["tree-view"] = cmd_tree_view,
|
||||
reset = cmd_reset,
|
||||
ask = cmd_ask,
|
||||
["ask-close"] = cmd_ask_close,
|
||||
["ask-toggle"] = cmd_ask_toggle,
|
||||
["ask-clear"] = cmd_ask_clear,
|
||||
gitignore = cmd_gitignore,
|
||||
}
|
||||
|
||||
local cmd_fn = commands[subcommand]
|
||||
if cmd_fn then
|
||||
cmd_fn(args)
|
||||
else
|
||||
utils.notify("Unknown subcommand: " .. subcommand, vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
|
||||
--- Setup all commands
|
||||
function M.setup()
|
||||
vim.api.nvim_create_user_command("Coder", coder_cmd, {
|
||||
nargs = "?",
|
||||
complete = function()
|
||||
return {
|
||||
"open", "close", "toggle", "process", "status", "focus",
|
||||
"tree", "tree-view", "reset", "gitignore",
|
||||
"ask", "ask-close", "ask-toggle", "ask-clear",
|
||||
}
|
||||
end,
|
||||
desc = "Codetyper.nvim commands",
|
||||
})
|
||||
|
||||
-- Convenience aliases
|
||||
vim.api.nvim_create_user_command("CoderOpen", function()
|
||||
cmd_open()
|
||||
end, { desc = "Open Coder view" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderClose", function()
|
||||
cmd_close()
|
||||
end, { desc = "Close Coder view" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderToggle", function()
|
||||
cmd_toggle()
|
||||
end, { desc = "Toggle Coder view" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderProcess", function()
|
||||
cmd_process()
|
||||
end, { desc = "Process prompt and generate code" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderTree", function()
|
||||
cmd_tree()
|
||||
end, { desc = "Refresh tree.log" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderTreeView", function()
|
||||
cmd_tree_view()
|
||||
end, { desc = "View tree.log" })
|
||||
|
||||
-- Ask panel commands
|
||||
vim.api.nvim_create_user_command("CoderAsk", function()
|
||||
cmd_ask()
|
||||
end, { desc = "Open Ask panel" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderAskToggle", function()
|
||||
cmd_ask_toggle()
|
||||
end, { desc = "Toggle Ask panel" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderAskClear", function()
|
||||
cmd_ask_clear()
|
||||
end, { desc = "Clear Ask history" })
|
||||
end
|
||||
|
||||
return M
|
||||
84
lua/codetyper/config.lua
Normal file
84
lua/codetyper/config.lua
Normal file
@@ -0,0 +1,84 @@
|
||||
---@mod codetyper.config Configuration module for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type CoderConfig
|
||||
local defaults = {
|
||||
llm = {
|
||||
provider = "claude",
|
||||
claude = {
|
||||
api_key = nil, -- Will use ANTHROPIC_API_KEY env var if nil
|
||||
model = "claude-sonnet-4-20250514",
|
||||
},
|
||||
ollama = {
|
||||
host = "http://localhost:11434",
|
||||
model = "codellama",
|
||||
},
|
||||
},
|
||||
window = {
|
||||
width = 0.25, -- 25% of screen width (1/4)
|
||||
position = "left",
|
||||
border = "rounded",
|
||||
},
|
||||
patterns = {
|
||||
open_tag = "/@",
|
||||
close_tag = "@/",
|
||||
file_pattern = "*.coder.*",
|
||||
},
|
||||
auto_gitignore = true,
|
||||
auto_open_ask = true, -- Auto-open Ask panel on startup
|
||||
}
|
||||
|
||||
--- Deep merge two tables
|
||||
---@param t1 table Base table
|
||||
---@param t2 table Table to merge into base
|
||||
---@return table Merged table
|
||||
local function deep_merge(t1, t2)
|
||||
local result = vim.deepcopy(t1)
|
||||
for k, v in pairs(t2) do
|
||||
if type(v) == "table" and type(result[k]) == "table" then
|
||||
result[k] = deep_merge(result[k], v)
|
||||
else
|
||||
result[k] = v
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
--- Setup configuration with user options
|
||||
---@param opts? CoderConfig User configuration options
|
||||
---@return CoderConfig Final configuration
|
||||
function M.setup(opts)
|
||||
opts = opts or {}
|
||||
return deep_merge(defaults, opts)
|
||||
end
|
||||
|
||||
--- Get default configuration
|
||||
---@return CoderConfig Default configuration
|
||||
function M.get_defaults()
|
||||
return vim.deepcopy(defaults)
|
||||
end
|
||||
|
||||
--- Validate configuration
|
||||
---@param config CoderConfig Configuration to validate
|
||||
---@return boolean, string? Valid status and optional error message
|
||||
function M.validate(config)
|
||||
if not config.llm then
|
||||
return false, "Missing LLM configuration"
|
||||
end
|
||||
|
||||
if config.llm.provider ~= "claude" and config.llm.provider ~= "ollama" then
|
||||
return false, "Invalid LLM provider. Must be 'claude' or 'ollama'"
|
||||
end
|
||||
|
||||
if config.llm.provider == "claude" then
|
||||
local api_key = config.llm.claude.api_key or vim.env.ANTHROPIC_API_KEY
|
||||
if not api_key or api_key == "" then
|
||||
return false, "Claude API key not configured. Set llm.claude.api_key or ANTHROPIC_API_KEY env var"
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return M
|
||||
190
lua/codetyper/gitignore.lua
Normal file
190
lua/codetyper/gitignore.lua
Normal file
@@ -0,0 +1,190 @@
|
||||
---@mod codetyper.gitignore Gitignore management for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
|
||||
--- Patterns to add to .gitignore
|
||||
local IGNORE_PATTERNS = {
|
||||
"*.coder.*",
|
||||
".coder/",
|
||||
}
|
||||
|
||||
--- Comment to identify codetyper entries
|
||||
local CODER_COMMENT = "# Codetyper.nvim - AI coding partner files"
|
||||
|
||||
--- Check if pattern exists in gitignore content
|
||||
---@param content string Gitignore content
|
||||
---@param pattern string Pattern to check
|
||||
---@return boolean
|
||||
local function pattern_exists(content, pattern)
|
||||
local escaped = utils.escape_pattern(pattern)
|
||||
return content:match("\n" .. escaped .. "\n") ~= nil
|
||||
or content:match("^" .. escaped .. "\n") ~= nil
|
||||
or content:match("\n" .. escaped .. "$") ~= nil
|
||||
or content == pattern
|
||||
end
|
||||
|
||||
--- Check if all patterns exist in gitignore content
|
||||
---@param content string Gitignore content
|
||||
---@return boolean, string[] All exist status and list of missing patterns
|
||||
local function all_patterns_exist(content)
|
||||
local missing = {}
|
||||
for _, pattern in ipairs(IGNORE_PATTERNS) do
|
||||
if not pattern_exists(content, pattern) then
|
||||
table.insert(missing, pattern)
|
||||
end
|
||||
end
|
||||
return #missing == 0, missing
|
||||
end
|
||||
|
||||
--- Get the path to .gitignore in project root
|
||||
---@return string|nil Path to .gitignore or nil
|
||||
function M.get_gitignore_path()
|
||||
local root = utils.get_project_root()
|
||||
if not root then
|
||||
return nil
|
||||
end
|
||||
return root .. "/.gitignore"
|
||||
end
|
||||
|
||||
--- Check if coder files are already ignored
|
||||
---@return boolean
|
||||
function M.is_ignored()
|
||||
local gitignore_path = M.get_gitignore_path()
|
||||
if not gitignore_path then
|
||||
return false
|
||||
end
|
||||
|
||||
local content = utils.read_file(gitignore_path)
|
||||
if not content then
|
||||
return false
|
||||
end
|
||||
|
||||
local all_exist, _ = all_patterns_exist(content)
|
||||
return all_exist
|
||||
end
|
||||
|
||||
--- Add coder patterns to .gitignore
|
||||
---@return boolean Success status
|
||||
function M.add_to_gitignore()
|
||||
local gitignore_path = M.get_gitignore_path()
|
||||
if not gitignore_path then
|
||||
utils.notify("Could not determine project root", vim.log.levels.WARN)
|
||||
return false
|
||||
end
|
||||
|
||||
local content = utils.read_file(gitignore_path)
|
||||
local patterns_to_add = {}
|
||||
|
||||
if content then
|
||||
-- File exists, check which patterns are missing
|
||||
local _, missing = all_patterns_exist(content)
|
||||
if #missing == 0 then
|
||||
return true -- All already ignored
|
||||
end
|
||||
patterns_to_add = missing
|
||||
else
|
||||
-- Create new .gitignore with all patterns
|
||||
content = ""
|
||||
patterns_to_add = IGNORE_PATTERNS
|
||||
end
|
||||
|
||||
-- Build the patterns string
|
||||
local patterns_str = table.concat(patterns_to_add, "\n")
|
||||
|
||||
if content == "" then
|
||||
-- New file
|
||||
content = CODER_COMMENT .. "\n" .. patterns_str .. "\n"
|
||||
else
|
||||
-- Append to existing
|
||||
local newline = content:sub(-1) == "\n" and "" or "\n"
|
||||
-- Check if comment already exists
|
||||
if not content:match(utils.escape_pattern(CODER_COMMENT)) then
|
||||
content = content .. newline .. "\n" .. CODER_COMMENT .. "\n" .. patterns_str .. "\n"
|
||||
else
|
||||
content = content .. newline .. patterns_str .. "\n"
|
||||
end
|
||||
end
|
||||
|
||||
if utils.write_file(gitignore_path, content) then
|
||||
utils.notify("Added coder patterns to .gitignore")
|
||||
return true
|
||||
else
|
||||
utils.notify("Failed to update .gitignore", vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
--- Ensure coder files are in .gitignore (called on setup)
|
||||
---@param auto_gitignore? boolean Override auto_gitignore setting (default: true)
|
||||
---@return boolean Success status
|
||||
function M.ensure_ignored(auto_gitignore)
|
||||
-- Default to true if not specified
|
||||
if auto_gitignore == nil then
|
||||
-- Try to get from config if available
|
||||
local ok, codetyper = pcall(require, "codetyper")
|
||||
if ok and codetyper.is_initialized and codetyper.is_initialized() then
|
||||
local config = codetyper.get_config()
|
||||
auto_gitignore = config and config.auto_gitignore
|
||||
else
|
||||
auto_gitignore = true -- Default to true
|
||||
end
|
||||
end
|
||||
|
||||
if not auto_gitignore then
|
||||
return true
|
||||
end
|
||||
|
||||
if M.is_ignored() then
|
||||
return true
|
||||
end
|
||||
|
||||
return M.add_to_gitignore()
|
||||
end
|
||||
|
||||
--- Remove coder patterns from .gitignore
|
||||
---@return boolean Success status
|
||||
function M.remove_from_gitignore()
|
||||
local gitignore_path = M.get_gitignore_path()
|
||||
if not gitignore_path then
|
||||
return false
|
||||
end
|
||||
|
||||
local content = utils.read_file(gitignore_path)
|
||||
if not content then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Remove the comment and all patterns
|
||||
content = content:gsub(CODER_COMMENT .. "\n", "")
|
||||
for _, pattern in ipairs(IGNORE_PATTERNS) do
|
||||
content = content:gsub(utils.escape_pattern(pattern) .. "\n?", "")
|
||||
end
|
||||
|
||||
-- Clean up extra newlines
|
||||
content = content:gsub("\n\n\n+", "\n\n")
|
||||
|
||||
return utils.write_file(gitignore_path, content)
|
||||
end
|
||||
|
||||
--- Get list of patterns being ignored
|
||||
---@return string[] List of patterns
|
||||
function M.get_ignore_patterns()
|
||||
return vim.deepcopy(IGNORE_PATTERNS)
|
||||
end
|
||||
|
||||
--- Force update gitignore (manual trigger)
|
||||
---@return boolean Success status
|
||||
function M.force_update()
|
||||
local gitignore_path = M.get_gitignore_path()
|
||||
if not gitignore_path then
|
||||
utils.notify("Could not determine project root for .gitignore", vim.log.levels.WARN)
|
||||
return false
|
||||
end
|
||||
|
||||
utils.notify("Updating .gitignore at: " .. gitignore_path)
|
||||
return M.add_to_gitignore()
|
||||
end
|
||||
|
||||
return M
|
||||
91
lua/codetyper/health.lua
Normal file
91
lua/codetyper/health.lua
Normal file
@@ -0,0 +1,91 @@
|
||||
---@mod codetyper.health Health check for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local health = vim.health or require("health")
|
||||
|
||||
--- Run health checks
|
||||
function M.check()
|
||||
health.start("Codetyper.nvim")
|
||||
|
||||
-- Check Neovim version
|
||||
if vim.fn.has("nvim-0.8.0") == 1 then
|
||||
health.ok("Neovim version >= 0.8.0")
|
||||
else
|
||||
health.error("Neovim 0.8.0+ required")
|
||||
end
|
||||
|
||||
-- Check if plugin is initialized
|
||||
local ok, codetyper = pcall(require, "codetyper")
|
||||
if ok and codetyper.is_initialized() then
|
||||
health.ok("Plugin initialized")
|
||||
else
|
||||
health.info("Plugin not yet initialized (call setup() first)")
|
||||
end
|
||||
|
||||
-- Check curl availability
|
||||
if vim.fn.executable("curl") == 1 then
|
||||
health.ok("curl is available")
|
||||
else
|
||||
health.error("curl is required for LLM API calls")
|
||||
end
|
||||
|
||||
-- Check LLM configuration
|
||||
if ok and codetyper.is_initialized() then
|
||||
local config = codetyper.get_config()
|
||||
|
||||
health.info("LLM Provider: " .. config.llm.provider)
|
||||
|
||||
if config.llm.provider == "claude" then
|
||||
local api_key = config.llm.claude.api_key or vim.env.ANTHROPIC_API_KEY
|
||||
if api_key and api_key ~= "" then
|
||||
health.ok("Claude API key configured")
|
||||
else
|
||||
health.warn("Claude API key not set. Set ANTHROPIC_API_KEY or llm.claude.api_key")
|
||||
end
|
||||
health.info("Claude model: " .. config.llm.claude.model)
|
||||
elseif config.llm.provider == "ollama" then
|
||||
health.info("Ollama host: " .. config.llm.ollama.host)
|
||||
health.info("Ollama model: " .. config.llm.ollama.model)
|
||||
|
||||
-- Try to check Ollama connectivity
|
||||
local ollama = require("codetyper.llm.ollama")
|
||||
ollama.health_check(function(is_ok, err)
|
||||
if is_ok then
|
||||
vim.schedule(function()
|
||||
health.ok("Ollama is reachable")
|
||||
end)
|
||||
else
|
||||
vim.schedule(function()
|
||||
health.warn("Cannot connect to Ollama: " .. (err or "unknown error"))
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- Check optional dependencies
|
||||
if pcall(require, "telescope") then
|
||||
health.ok("telescope.nvim is available (enhanced file picker)")
|
||||
else
|
||||
health.info("telescope.nvim not found (using basic file picker)")
|
||||
end
|
||||
|
||||
-- Check .gitignore configuration
|
||||
local utils = require("codetyper.utils")
|
||||
local gitignore = require("codetyper.gitignore")
|
||||
|
||||
local root = utils.get_project_root()
|
||||
if root then
|
||||
health.info("Project root: " .. root)
|
||||
if gitignore.is_ignored() then
|
||||
health.ok("Coder files are in .gitignore")
|
||||
else
|
||||
health.warn("Coder files not in .gitignore (will be added on setup)")
|
||||
end
|
||||
else
|
||||
health.info("No project root detected")
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
69
lua/codetyper/init.lua
Normal file
69
lua/codetyper/init.lua
Normal file
@@ -0,0 +1,69 @@
|
||||
---@mod codetyper Codetyper.nvim - AI-powered coding partner
|
||||
---@brief [[
|
||||
--- Codetyper.nvim is a Neovim plugin that acts as your coding partner.
|
||||
--- It uses LLM APIs (Claude, Ollama) to help you write code faster
|
||||
--- using special `.coder.*` files and inline prompt tags.
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type CoderConfig
|
||||
M.config = {}
|
||||
|
||||
---@type boolean
|
||||
M._initialized = false
|
||||
|
||||
--- Setup the plugin with user configuration
|
||||
---@param opts? CoderConfig User configuration options
|
||||
function M.setup(opts)
|
||||
if M._initialized then
|
||||
return
|
||||
end
|
||||
|
||||
local config = require("codetyper.config")
|
||||
M.config = config.setup(opts)
|
||||
|
||||
-- Initialize modules
|
||||
local commands = require("codetyper.commands")
|
||||
local gitignore = require("codetyper.gitignore")
|
||||
local autocmds = require("codetyper.autocmds")
|
||||
local tree = require("codetyper.tree")
|
||||
|
||||
-- Register commands
|
||||
commands.setup()
|
||||
|
||||
-- Setup autocommands
|
||||
autocmds.setup()
|
||||
|
||||
-- Ensure .gitignore has coder files excluded
|
||||
gitignore.ensure_ignored()
|
||||
|
||||
-- Initialize tree logging (creates .coder folder and initial tree.log)
|
||||
tree.setup()
|
||||
|
||||
M._initialized = true
|
||||
|
||||
-- Auto-open Ask panel after a short delay (to let UI settle)
|
||||
if M.config.auto_open_ask then
|
||||
vim.defer_fn(function()
|
||||
local ask = require("codetyper.ask")
|
||||
if not ask.is_open() then
|
||||
ask.open()
|
||||
end
|
||||
end, 300)
|
||||
end
|
||||
end
|
||||
|
||||
--- Get current configuration
|
||||
---@return CoderConfig
|
||||
function M.get_config()
|
||||
return M.config
|
||||
end
|
||||
|
||||
--- Check if plugin is initialized
|
||||
---@return boolean
|
||||
function M.is_initialized()
|
||||
return M._initialized
|
||||
end
|
||||
|
||||
return M
|
||||
239
lua/codetyper/inject.lua
Normal file
239
lua/codetyper/inject.lua
Normal file
@@ -0,0 +1,239 @@
|
||||
---@mod codetyper.inject Code injection for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
|
||||
--- Inject generated code into target file
|
||||
---@param target_path string Path to target file
|
||||
---@param code string Generated code
|
||||
---@param prompt_type string Type of prompt (refactor, add, document, etc.)
|
||||
function M.inject_code(target_path, code, prompt_type)
|
||||
local window = require("codetyper.window")
|
||||
|
||||
-- Normalize the target path
|
||||
target_path = vim.fn.fnamemodify(target_path, ":p")
|
||||
|
||||
-- Get target buffer
|
||||
local target_buf = window.get_target_buf()
|
||||
|
||||
if not target_buf or not vim.api.nvim_buf_is_valid(target_buf) then
|
||||
-- Try to find buffer by path
|
||||
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
||||
local buf_name = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf), ":p")
|
||||
if buf_name == target_path then
|
||||
target_buf = buf
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- If still not found, open the file
|
||||
if not target_buf or not vim.api.nvim_buf_is_valid(target_buf) then
|
||||
-- Check if file exists
|
||||
if utils.file_exists(target_path) then
|
||||
vim.cmd("edit " .. vim.fn.fnameescape(target_path))
|
||||
target_buf = vim.api.nvim_get_current_buf()
|
||||
utils.notify("Opened target file: " .. vim.fn.fnamemodify(target_path, ":t"))
|
||||
else
|
||||
utils.notify("Target file not found: " .. target_path, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if not target_buf then
|
||||
utils.notify("Target buffer not found", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
utils.notify("Injecting code into: " .. vim.fn.fnamemodify(target_path, ":t"))
|
||||
|
||||
-- Different injection strategies based on prompt type
|
||||
if prompt_type == "refactor" then
|
||||
M.inject_refactor(target_buf, code)
|
||||
elseif prompt_type == "add" then
|
||||
M.inject_add(target_buf, code)
|
||||
elseif prompt_type == "document" then
|
||||
M.inject_document(target_buf, code)
|
||||
else
|
||||
-- For generic, auto-add instead of prompting
|
||||
M.inject_add(target_buf, code)
|
||||
end
|
||||
|
||||
-- Mark buffer as modified and save
|
||||
vim.bo[target_buf].modified = true
|
||||
|
||||
-- Auto-save the target file
|
||||
vim.schedule(function()
|
||||
if vim.api.nvim_buf_is_valid(target_buf) then
|
||||
local wins = vim.fn.win_findbuf(target_buf)
|
||||
if #wins > 0 then
|
||||
vim.api.nvim_win_call(wins[1], function()
|
||||
vim.cmd("silent! write")
|
||||
end)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Inject code for refactor (replace entire file)
|
||||
---@param bufnr number Buffer number
|
||||
---@param code string Generated code
|
||||
function M.inject_refactor(bufnr, code)
|
||||
local lines = vim.split(code, "\n", { plain = true })
|
||||
|
||||
-- Save cursor position
|
||||
local cursor = nil
|
||||
local wins = vim.fn.win_findbuf(bufnr)
|
||||
if #wins > 0 then
|
||||
cursor = vim.api.nvim_win_get_cursor(wins[1])
|
||||
end
|
||||
|
||||
-- Replace buffer content
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||
|
||||
-- Restore cursor position if possible
|
||||
if cursor then
|
||||
local line_count = vim.api.nvim_buf_line_count(bufnr)
|
||||
cursor[1] = math.min(cursor[1], line_count)
|
||||
pcall(vim.api.nvim_win_set_cursor, wins[1], cursor)
|
||||
end
|
||||
|
||||
utils.notify("Code refactored", vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
--- Inject code for add (append at cursor or end)
|
||||
---@param bufnr number Buffer number
|
||||
---@param code string Generated code
|
||||
function M.inject_add(bufnr, code)
|
||||
local lines = vim.split(code, "\n", { plain = true })
|
||||
|
||||
-- Get cursor position in target window
|
||||
local window = require("codetyper.window")
|
||||
local target_win = window.get_target_win()
|
||||
|
||||
local insert_line
|
||||
if target_win and vim.api.nvim_win_is_valid(target_win) then
|
||||
local cursor = vim.api.nvim_win_get_cursor(target_win)
|
||||
insert_line = cursor[1]
|
||||
else
|
||||
-- Append at end
|
||||
insert_line = vim.api.nvim_buf_line_count(bufnr)
|
||||
end
|
||||
|
||||
-- Insert lines at position
|
||||
vim.api.nvim_buf_set_lines(bufnr, insert_line, insert_line, false, lines)
|
||||
|
||||
utils.notify("Code added at line " .. (insert_line + 1), vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
--- Inject documentation
|
||||
---@param bufnr number Buffer number
|
||||
---@param code string Generated documentation
|
||||
function M.inject_document(bufnr, code)
|
||||
-- Documentation typically goes above the current function/class
|
||||
-- For simplicity, insert at cursor position
|
||||
M.inject_add(bufnr, code)
|
||||
utils.notify("Documentation added", vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
--- Generic injection (prompt user for action)
|
||||
---@param bufnr number Buffer number
|
||||
---@param code string Generated code
|
||||
function M.inject_generic(bufnr, code)
|
||||
local actions = {
|
||||
"Replace entire file",
|
||||
"Insert at cursor",
|
||||
"Append to end",
|
||||
"Copy to clipboard",
|
||||
"Cancel",
|
||||
}
|
||||
|
||||
vim.ui.select(actions, {
|
||||
prompt = "How to inject the generated code?",
|
||||
}, function(choice)
|
||||
if not choice then
|
||||
return
|
||||
end
|
||||
|
||||
if choice == "Replace entire file" then
|
||||
M.inject_refactor(bufnr, code)
|
||||
elseif choice == "Insert at cursor" then
|
||||
M.inject_add(bufnr, code)
|
||||
elseif choice == "Append to end" then
|
||||
local lines = vim.split(code, "\n", { plain = true })
|
||||
local line_count = vim.api.nvim_buf_line_count(bufnr)
|
||||
vim.api.nvim_buf_set_lines(bufnr, line_count, line_count, false, lines)
|
||||
utils.notify("Code appended to end", vim.log.levels.INFO)
|
||||
elseif choice == "Copy to clipboard" then
|
||||
vim.fn.setreg("+", code)
|
||||
utils.notify("Code copied to clipboard", vim.log.levels.INFO)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Preview code in a floating window before injection
|
||||
---@param code string Generated code
|
||||
---@param callback fun(action: string) Callback with selected action
|
||||
function M.preview(code, callback)
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
local lines = vim.split(code, "\n", { plain = true })
|
||||
|
||||
-- Create buffer for preview
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
|
||||
-- Calculate window size
|
||||
local width = math.min(80, vim.o.columns - 10)
|
||||
local height = math.min(#lines + 2, vim.o.lines - 10)
|
||||
|
||||
-- Create floating window
|
||||
local win = vim.api.nvim_open_win(buf, true, {
|
||||
relative = "editor",
|
||||
width = width,
|
||||
height = height,
|
||||
row = math.floor((vim.o.lines - height) / 2),
|
||||
col = math.floor((vim.o.columns - width) / 2),
|
||||
style = "minimal",
|
||||
border = config.window.border,
|
||||
title = " Generated Code Preview ",
|
||||
title_pos = "center",
|
||||
})
|
||||
|
||||
-- Set buffer options
|
||||
vim.bo[buf].modifiable = false
|
||||
vim.bo[buf].bufhidden = "wipe"
|
||||
|
||||
-- Add keymaps for actions
|
||||
local opts = { buffer = buf, noremap = true, silent = true }
|
||||
|
||||
vim.keymap.set("n", "q", function()
|
||||
vim.api.nvim_win_close(win, true)
|
||||
callback("cancel")
|
||||
end, opts)
|
||||
|
||||
vim.keymap.set("n", "<CR>", function()
|
||||
vim.api.nvim_win_close(win, true)
|
||||
callback("inject")
|
||||
end, opts)
|
||||
|
||||
vim.keymap.set("n", "y", function()
|
||||
vim.fn.setreg("+", code)
|
||||
utils.notify("Copied to clipboard")
|
||||
end, opts)
|
||||
|
||||
-- Show help in command line
|
||||
vim.api.nvim_echo({
|
||||
{ "Press ", "Normal" },
|
||||
{ "<CR>", "Keyword" },
|
||||
{ " to inject, ", "Normal" },
|
||||
{ "y", "Keyword" },
|
||||
{ " to copy, ", "Normal" },
|
||||
{ "q", "Keyword" },
|
||||
{ " to cancel", "Normal" },
|
||||
}, false, {})
|
||||
end
|
||||
|
||||
return M
|
||||
154
lua/codetyper/llm/claude.lua
Normal file
154
lua/codetyper/llm/claude.lua
Normal file
@@ -0,0 +1,154 @@
|
||||
---@mod codetyper.llm.claude Claude API client for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local llm = require("codetyper.llm")
|
||||
|
||||
--- Claude API endpoint
|
||||
local API_URL = "https://api.anthropic.com/v1/messages"
|
||||
|
||||
--- Get API key from config or environment
|
||||
---@return string|nil API key
|
||||
local function get_api_key()
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
return config.llm.claude.api_key or vim.env.ANTHROPIC_API_KEY
|
||||
end
|
||||
|
||||
--- Get model from config
|
||||
---@return string Model name
|
||||
local function get_model()
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
return config.llm.claude.model
|
||||
end
|
||||
|
||||
--- Build request body for Claude API
|
||||
---@param prompt string User prompt
|
||||
---@param context table Context information
|
||||
---@return table Request body
|
||||
local function build_request_body(prompt, context)
|
||||
local system_prompt = llm.build_system_prompt(context)
|
||||
|
||||
return {
|
||||
model = get_model(),
|
||||
max_tokens = 4096,
|
||||
system = system_prompt,
|
||||
messages = {
|
||||
{
|
||||
role = "user",
|
||||
content = prompt,
|
||||
},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
--- Make HTTP request to Claude API
|
||||
---@param body table Request body
|
||||
---@param callback fun(response: string|nil, error: string|nil) Callback function
|
||||
local function make_request(body, callback)
|
||||
local api_key = get_api_key()
|
||||
if not api_key then
|
||||
callback(nil, "Claude API key not configured")
|
||||
return
|
||||
end
|
||||
|
||||
local json_body = vim.json.encode(body)
|
||||
|
||||
-- Use curl for HTTP request (plenary.curl alternative)
|
||||
local cmd = {
|
||||
"curl",
|
||||
"-s",
|
||||
"-X", "POST",
|
||||
API_URL,
|
||||
"-H", "Content-Type: application/json",
|
||||
"-H", "x-api-key: " .. api_key,
|
||||
"-H", "anthropic-version: 2023-06-01",
|
||||
"-d", json_body,
|
||||
}
|
||||
|
||||
vim.fn.jobstart(cmd, {
|
||||
stdout_buffered = true,
|
||||
on_stdout = function(_, data)
|
||||
if not data or #data == 0 or (data[1] == "" and #data == 1) then
|
||||
return
|
||||
end
|
||||
|
||||
local response_text = table.concat(data, "\n")
|
||||
local ok, response = pcall(vim.json.decode, response_text)
|
||||
|
||||
if not ok then
|
||||
vim.schedule(function()
|
||||
callback(nil, "Failed to parse Claude response")
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if response.error then
|
||||
vim.schedule(function()
|
||||
callback(nil, response.error.message or "Claude API error")
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if response.content and response.content[1] and response.content[1].text then
|
||||
local code = llm.extract_code(response.content[1].text)
|
||||
vim.schedule(function()
|
||||
callback(code, nil)
|
||||
end)
|
||||
else
|
||||
vim.schedule(function()
|
||||
callback(nil, "No content in Claude response")
|
||||
end)
|
||||
end
|
||||
end,
|
||||
on_stderr = function(_, data)
|
||||
if data and #data > 0 and data[1] ~= "" then
|
||||
vim.schedule(function()
|
||||
callback(nil, "Claude API request failed: " .. table.concat(data, "\n"))
|
||||
end)
|
||||
end
|
||||
end,
|
||||
on_exit = function(_, code)
|
||||
if code ~= 0 then
|
||||
vim.schedule(function()
|
||||
callback(nil, "Claude API request failed with code: " .. code)
|
||||
end)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
--- Generate code using Claude API
|
||||
---@param prompt string The user's prompt
|
||||
---@param context table Context information
|
||||
---@param callback fun(response: string|nil, error: string|nil) Callback function
|
||||
function M.generate(prompt, context, callback)
|
||||
utils.notify("Sending request to Claude...", vim.log.levels.INFO)
|
||||
|
||||
local body = build_request_body(prompt, context)
|
||||
make_request(body, function(response, err)
|
||||
if err then
|
||||
utils.notify(err, vim.log.levels.ERROR)
|
||||
callback(nil, err)
|
||||
else
|
||||
utils.notify("Code generated successfully", vim.log.levels.INFO)
|
||||
callback(response, nil)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Check if Claude is properly configured
|
||||
---@return boolean, string? Valid status and optional error message
|
||||
function M.validate()
|
||||
local api_key = get_api_key()
|
||||
if not api_key or api_key == "" then
|
||||
return false, "Claude API key not configured"
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return M
|
||||
101
lua/codetyper/llm/init.lua
Normal file
101
lua/codetyper/llm/init.lua
Normal file
@@ -0,0 +1,101 @@
|
||||
---@mod codetyper.llm LLM interface for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
|
||||
--- Get the appropriate LLM client based on configuration
|
||||
---@return table LLM client module
|
||||
function M.get_client()
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
if config.llm.provider == "claude" then
|
||||
return require("codetyper.llm.claude")
|
||||
elseif config.llm.provider == "ollama" then
|
||||
return require("codetyper.llm.ollama")
|
||||
else
|
||||
error("Unknown LLM provider: " .. config.llm.provider)
|
||||
end
|
||||
end
|
||||
|
||||
--- Generate code from a prompt
|
||||
---@param prompt string The user's prompt
|
||||
---@param context table Context information (file content, language, etc.)
|
||||
---@param callback fun(response: string|nil, error: string|nil) Callback function
|
||||
function M.generate(prompt, context, callback)
|
||||
local client = M.get_client()
|
||||
client.generate(prompt, context, callback)
|
||||
end
|
||||
|
||||
--- Build the system prompt for code generation
|
||||
---@param context table Context information
|
||||
---@return string System prompt
|
||||
function M.build_system_prompt(context)
|
||||
local prompts = require("codetyper.prompts")
|
||||
|
||||
-- Select appropriate system prompt based on context
|
||||
local prompt_type = context.prompt_type or "code_generation"
|
||||
local system_prompts = prompts.system
|
||||
|
||||
local system = system_prompts[prompt_type] or system_prompts.code_generation
|
||||
|
||||
-- Substitute variables
|
||||
system = system:gsub("{{language}}", context.language or "unknown")
|
||||
system = system:gsub("{{filepath}}", context.file_path or "unknown")
|
||||
|
||||
if context.file_content then
|
||||
system = system .. "\n\nExisting file content:\n```\n" .. context.file_content .. "\n```"
|
||||
end
|
||||
|
||||
return system
|
||||
end
|
||||
|
||||
--- Build context for LLM request
|
||||
---@param target_path string Path to target file
|
||||
---@param prompt_type string Type of prompt
|
||||
---@return table Context object
|
||||
function M.build_context(target_path, prompt_type)
|
||||
local content = utils.read_file(target_path)
|
||||
local ext = vim.fn.fnamemodify(target_path, ":e")
|
||||
|
||||
-- Map extension to language
|
||||
local lang_map = {
|
||||
ts = "TypeScript",
|
||||
tsx = "TypeScript React",
|
||||
js = "JavaScript",
|
||||
jsx = "JavaScript React",
|
||||
py = "Python",
|
||||
lua = "Lua",
|
||||
go = "Go",
|
||||
rs = "Rust",
|
||||
rb = "Ruby",
|
||||
java = "Java",
|
||||
c = "C",
|
||||
cpp = "C++",
|
||||
cs = "C#",
|
||||
}
|
||||
|
||||
return {
|
||||
file_content = content,
|
||||
language = lang_map[ext] or ext,
|
||||
extension = ext,
|
||||
prompt_type = prompt_type,
|
||||
file_path = target_path,
|
||||
}
|
||||
end
|
||||
|
||||
--- Parse LLM response and extract code
|
||||
---@param response string Raw LLM response
|
||||
---@return string Extracted code
|
||||
function M.extract_code(response)
|
||||
-- Remove markdown code blocks if present
|
||||
local code = response:gsub("```%w*\n?", ""):gsub("\n?```", "")
|
||||
|
||||
-- Trim whitespace
|
||||
code = code:match("^%s*(.-)%s*$")
|
||||
|
||||
return code
|
||||
end
|
||||
|
||||
return M
|
||||
173
lua/codetyper/llm/ollama.lua
Normal file
173
lua/codetyper/llm/ollama.lua
Normal file
@@ -0,0 +1,173 @@
|
||||
---@mod codetyper.llm.ollama Ollama API client for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
local llm = require("codetyper.llm")
|
||||
|
||||
--- Get Ollama host from config
|
||||
---@return string Host URL
|
||||
local function get_host()
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
return config.llm.ollama.host
|
||||
end
|
||||
|
||||
--- Get model from config
|
||||
---@return string Model name
|
||||
local function get_model()
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
return config.llm.ollama.model
|
||||
end
|
||||
|
||||
--- Build request body for Ollama API
|
||||
---@param prompt string User prompt
|
||||
---@param context table Context information
|
||||
---@return table Request body
|
||||
local function build_request_body(prompt, context)
|
||||
local system_prompt = llm.build_system_prompt(context)
|
||||
|
||||
return {
|
||||
model = get_model(),
|
||||
system = system_prompt,
|
||||
prompt = prompt,
|
||||
stream = false,
|
||||
options = {
|
||||
temperature = 0.2,
|
||||
num_predict = 4096,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
--- Make HTTP request to Ollama API
|
||||
---@param body table Request body
|
||||
---@param callback fun(response: string|nil, error: string|nil) Callback function
|
||||
local function make_request(body, callback)
|
||||
local host = get_host()
|
||||
local url = host .. "/api/generate"
|
||||
local json_body = vim.json.encode(body)
|
||||
|
||||
local cmd = {
|
||||
"curl",
|
||||
"-s",
|
||||
"-X", "POST",
|
||||
url,
|
||||
"-H", "Content-Type: application/json",
|
||||
"-d", json_body,
|
||||
}
|
||||
|
||||
vim.fn.jobstart(cmd, {
|
||||
stdout_buffered = true,
|
||||
on_stdout = function(_, data)
|
||||
if not data or #data == 0 or (data[1] == "" and #data == 1) then
|
||||
return
|
||||
end
|
||||
|
||||
local response_text = table.concat(data, "\n")
|
||||
local ok, response = pcall(vim.json.decode, response_text)
|
||||
|
||||
if not ok then
|
||||
vim.schedule(function()
|
||||
callback(nil, "Failed to parse Ollama response")
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if response.error then
|
||||
vim.schedule(function()
|
||||
callback(nil, response.error or "Ollama API error")
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
if response.response then
|
||||
local code = llm.extract_code(response.response)
|
||||
vim.schedule(function()
|
||||
callback(code, nil)
|
||||
end)
|
||||
else
|
||||
vim.schedule(function()
|
||||
callback(nil, "No response from Ollama")
|
||||
end)
|
||||
end
|
||||
end,
|
||||
on_stderr = function(_, data)
|
||||
if data and #data > 0 and data[1] ~= "" then
|
||||
vim.schedule(function()
|
||||
callback(nil, "Ollama API request failed: " .. table.concat(data, "\n"))
|
||||
end)
|
||||
end
|
||||
end,
|
||||
on_exit = function(_, code)
|
||||
if code ~= 0 then
|
||||
vim.schedule(function()
|
||||
callback(nil, "Ollama API request failed with code: " .. code)
|
||||
end)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
--- Generate code using Ollama API
|
||||
---@param prompt string The user's prompt
|
||||
---@param context table Context information
|
||||
---@param callback fun(response: string|nil, error: string|nil) Callback function
|
||||
function M.generate(prompt, context, callback)
|
||||
utils.notify("Sending request to Ollama...", vim.log.levels.INFO)
|
||||
|
||||
local body = build_request_body(prompt, context)
|
||||
make_request(body, function(response, err)
|
||||
if err then
|
||||
utils.notify(err, vim.log.levels.ERROR)
|
||||
callback(nil, err)
|
||||
else
|
||||
utils.notify("Code generated successfully", vim.log.levels.INFO)
|
||||
callback(response, nil)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Check if Ollama is reachable
|
||||
---@param callback fun(ok: boolean, error: string|nil) Callback function
|
||||
function M.health_check(callback)
|
||||
local host = get_host()
|
||||
|
||||
local cmd = { "curl", "-s", host .. "/api/tags" }
|
||||
|
||||
vim.fn.jobstart(cmd, {
|
||||
stdout_buffered = true,
|
||||
on_stdout = function(_, data)
|
||||
if data and #data > 0 and data[1] ~= "" then
|
||||
vim.schedule(function()
|
||||
callback(true, nil)
|
||||
end)
|
||||
end
|
||||
end,
|
||||
on_exit = function(_, code)
|
||||
if code ~= 0 then
|
||||
vim.schedule(function()
|
||||
callback(false, "Cannot connect to Ollama at " .. host)
|
||||
end)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
--- Check if Ollama is properly configured
|
||||
---@return boolean, string? Valid status and optional error message
|
||||
function M.validate()
|
||||
local host = get_host()
|
||||
if not host or host == "" then
|
||||
return false, "Ollama host not configured"
|
||||
end
|
||||
local model = get_model()
|
||||
if not model or model == "" then
|
||||
return false, "Ollama model not configured"
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return M
|
||||
183
lua/codetyper/parser.lua
Normal file
183
lua/codetyper/parser.lua
Normal file
@@ -0,0 +1,183 @@
|
||||
---@mod codetyper.parser Parser for /@ @/ prompt tags
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
|
||||
--- Find all prompts in buffer content
|
||||
---@param content string Buffer content
|
||||
---@param open_tag string Opening tag
|
||||
---@param close_tag string Closing tag
|
||||
---@return CoderPrompt[] List of found prompts
|
||||
function M.find_prompts(content, open_tag, close_tag)
|
||||
local prompts = {}
|
||||
local escaped_open = utils.escape_pattern(open_tag)
|
||||
local escaped_close = utils.escape_pattern(close_tag)
|
||||
|
||||
local lines = vim.split(content, "\n", { plain = true })
|
||||
local in_prompt = false
|
||||
local current_prompt = nil
|
||||
local prompt_content = {}
|
||||
|
||||
for line_num, line in ipairs(lines) do
|
||||
if not in_prompt then
|
||||
-- Look for opening tag
|
||||
local start_col = line:find(escaped_open)
|
||||
if start_col then
|
||||
in_prompt = true
|
||||
current_prompt = {
|
||||
start_line = line_num,
|
||||
start_col = start_col,
|
||||
content = "",
|
||||
}
|
||||
-- Get content after opening tag on same line
|
||||
local after_tag = line:sub(start_col + #open_tag)
|
||||
local end_col = after_tag:find(escaped_close)
|
||||
if end_col then
|
||||
-- Single line prompt
|
||||
current_prompt.content = after_tag:sub(1, end_col - 1)
|
||||
current_prompt.end_line = line_num
|
||||
current_prompt.end_col = start_col + #open_tag + end_col + #close_tag - 2
|
||||
table.insert(prompts, current_prompt)
|
||||
in_prompt = false
|
||||
current_prompt = nil
|
||||
else
|
||||
table.insert(prompt_content, after_tag)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Look for closing tag
|
||||
local end_col = line:find(escaped_close)
|
||||
if end_col then
|
||||
-- Found closing tag
|
||||
local before_tag = line:sub(1, end_col - 1)
|
||||
table.insert(prompt_content, before_tag)
|
||||
current_prompt.content = table.concat(prompt_content, "\n")
|
||||
current_prompt.end_line = line_num
|
||||
current_prompt.end_col = end_col + #close_tag - 1
|
||||
table.insert(prompts, current_prompt)
|
||||
in_prompt = false
|
||||
current_prompt = nil
|
||||
prompt_content = {}
|
||||
else
|
||||
table.insert(prompt_content, line)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return prompts
|
||||
end
|
||||
|
||||
--- Find prompts in a buffer
|
||||
---@param bufnr number Buffer number
|
||||
---@return CoderPrompt[] List of found prompts
|
||||
function M.find_prompts_in_buffer(bufnr)
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local content = table.concat(lines, "\n")
|
||||
|
||||
return M.find_prompts(content, config.patterns.open_tag, config.patterns.close_tag)
|
||||
end
|
||||
|
||||
--- Get prompt at cursor position
|
||||
---@param bufnr? number Buffer number (default: current)
|
||||
---@return CoderPrompt|nil Prompt at cursor or nil
|
||||
function M.get_prompt_at_cursor(bufnr)
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||
local line = cursor[1]
|
||||
local col = cursor[2] + 1 -- Convert to 1-indexed
|
||||
|
||||
local prompts = M.find_prompts_in_buffer(bufnr)
|
||||
|
||||
for _, prompt in ipairs(prompts) do
|
||||
if line >= prompt.start_line and line <= prompt.end_line then
|
||||
if line == prompt.start_line and col < prompt.start_col then
|
||||
goto continue
|
||||
end
|
||||
if line == prompt.end_line and col > prompt.end_col then
|
||||
goto continue
|
||||
end
|
||||
return prompt
|
||||
end
|
||||
::continue::
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Get the last closed prompt in buffer
|
||||
---@param bufnr? number Buffer number (default: current)
|
||||
---@return CoderPrompt|nil Last prompt or nil
|
||||
function M.get_last_prompt(bufnr)
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
local prompts = M.find_prompts_in_buffer(bufnr)
|
||||
|
||||
if #prompts > 0 then
|
||||
return prompts[#prompts]
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Extract the prompt type from content
|
||||
---@param content string Prompt content
|
||||
---@return "refactor" | "add" | "document" | "explain" | "generic" Prompt type
|
||||
function M.detect_prompt_type(content)
|
||||
local lower = content:lower()
|
||||
|
||||
if lower:match("refactor") then
|
||||
return "refactor"
|
||||
elseif lower:match("add") or lower:match("create") or lower:match("implement") then
|
||||
return "add"
|
||||
elseif lower:match("document") or lower:match("comment") or lower:match("jsdoc") then
|
||||
return "document"
|
||||
elseif lower:match("explain") or lower:match("what") or lower:match("how") then
|
||||
return "explain"
|
||||
end
|
||||
|
||||
return "generic"
|
||||
end
|
||||
|
||||
--- Clean prompt content (trim whitespace, normalize newlines)
|
||||
---@param content string Raw prompt content
|
||||
---@return string Cleaned content
|
||||
function M.clean_prompt(content)
|
||||
-- Trim leading/trailing whitespace
|
||||
content = content:match("^%s*(.-)%s*$")
|
||||
-- Normalize multiple newlines
|
||||
content = content:gsub("\n\n\n+", "\n\n")
|
||||
return content
|
||||
end
|
||||
|
||||
--- Check if line contains a closing tag
|
||||
---@param line string Line to check
|
||||
---@param close_tag string Closing tag
|
||||
---@return boolean
|
||||
function M.has_closing_tag(line, close_tag)
|
||||
return line:find(utils.escape_pattern(close_tag)) ~= nil
|
||||
end
|
||||
|
||||
--- Check if buffer has any unclosed prompts
|
||||
---@param bufnr? number Buffer number (default: current)
|
||||
---@return boolean
|
||||
function M.has_unclosed_prompts(bufnr)
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local content = table.concat(lines, "\n")
|
||||
|
||||
local escaped_open = utils.escape_pattern(config.patterns.open_tag)
|
||||
local escaped_close = utils.escape_pattern(config.patterns.close_tag)
|
||||
|
||||
local _, open_count = content:gsub(escaped_open, "")
|
||||
local _, close_count = content:gsub(escaped_close, "")
|
||||
|
||||
return open_count > close_count
|
||||
end
|
||||
|
||||
return M
|
||||
128
lua/codetyper/prompts/ask.lua
Normal file
128
lua/codetyper/prompts/ask.lua
Normal file
@@ -0,0 +1,128 @@
|
||||
---@mod codetyper.prompts.ask Ask/explanation prompts for Codetyper.nvim
|
||||
---
|
||||
--- These prompts are used for the Ask panel and code explanations.
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Prompt for explaining code
|
||||
M.explain_code = [[Please explain the following code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Provide:
|
||||
1. A high-level overview of what it does
|
||||
2. Explanation of key parts
|
||||
3. Any potential issues or improvements
|
||||
]]
|
||||
|
||||
--- Prompt for explaining a specific function
|
||||
M.explain_function = [[Explain this function in detail:
|
||||
|
||||
{{code}}
|
||||
|
||||
Include:
|
||||
1. What the function does
|
||||
2. Parameters and their purposes
|
||||
3. Return value
|
||||
4. Any side effects
|
||||
5. Usage examples
|
||||
]]
|
||||
|
||||
--- Prompt for explaining an error
|
||||
M.explain_error = [[I'm getting this error:
|
||||
|
||||
{{error}}
|
||||
|
||||
In this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Please explain:
|
||||
1. What the error means
|
||||
2. Why it's happening
|
||||
3. How to fix it
|
||||
]]
|
||||
|
||||
--- Prompt for code review
|
||||
M.code_review = [[Please review this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Provide feedback on:
|
||||
1. Code quality and readability
|
||||
2. Potential bugs or issues
|
||||
3. Performance considerations
|
||||
4. Security concerns (if applicable)
|
||||
5. Suggested improvements
|
||||
]]
|
||||
|
||||
--- Prompt for explaining a concept
|
||||
M.explain_concept = [[Explain the following programming concept:
|
||||
|
||||
{{concept}}
|
||||
|
||||
Include:
|
||||
1. Definition and purpose
|
||||
2. When and why to use it
|
||||
3. Simple code examples
|
||||
4. Common pitfalls to avoid
|
||||
]]
|
||||
|
||||
--- Prompt for comparing approaches
|
||||
M.compare_approaches = [[Compare these different approaches:
|
||||
|
||||
{{approaches}}
|
||||
|
||||
Analyze:
|
||||
1. Pros and cons of each
|
||||
2. Performance implications
|
||||
3. Maintainability
|
||||
4. When to use each approach
|
||||
]]
|
||||
|
||||
--- Prompt for debugging help
|
||||
M.debug_help = [[Help me debug this issue:
|
||||
|
||||
Problem: {{problem}}
|
||||
|
||||
Code:
|
||||
{{code}}
|
||||
|
||||
What I've tried:
|
||||
{{attempts}}
|
||||
|
||||
Please help identify the issue and suggest a solution.
|
||||
]]
|
||||
|
||||
--- Prompt for architecture advice
|
||||
M.architecture_advice = [[I need advice on this architecture decision:
|
||||
|
||||
{{question}}
|
||||
|
||||
Context:
|
||||
{{context}}
|
||||
|
||||
Please provide:
|
||||
1. Recommended approach
|
||||
2. Reasoning
|
||||
3. Potential alternatives
|
||||
4. Things to consider
|
||||
]]
|
||||
|
||||
--- Generic ask prompt
|
||||
M.generic = [[USER QUESTION: {{question}}
|
||||
|
||||
{{#if files}}
|
||||
ATTACHED FILE CONTENTS:
|
||||
{{files}}
|
||||
{{/if}}
|
||||
|
||||
{{#if context}}
|
||||
ADDITIONAL CONTEXT:
|
||||
{{context}}
|
||||
{{/if}}
|
||||
|
||||
Please provide a helpful, accurate response.
|
||||
]]
|
||||
|
||||
return M
|
||||
93
lua/codetyper/prompts/code.lua
Normal file
93
lua/codetyper/prompts/code.lua
Normal file
@@ -0,0 +1,93 @@
|
||||
---@mod codetyper.prompts.code Code generation prompts for Codetyper.nvim
|
||||
---
|
||||
--- These prompts are used for generating new code.
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Prompt template for creating a new function
|
||||
M.create_function = [[Create a function with the following requirements:
|
||||
|
||||
{{description}}
|
||||
|
||||
Requirements:
|
||||
- Follow the coding style of the existing file
|
||||
- Include proper error handling
|
||||
- Use appropriate types (if applicable)
|
||||
- Make it efficient and readable
|
||||
]]
|
||||
|
||||
--- Prompt template for creating a new class/module
|
||||
M.create_class = [[Create a class/module with the following requirements:
|
||||
|
||||
{{description}}
|
||||
|
||||
Requirements:
|
||||
- Follow OOP best practices
|
||||
- Include constructor/initialization
|
||||
- Implement proper encapsulation
|
||||
- Add necessary methods as described
|
||||
]]
|
||||
|
||||
--- Prompt template for implementing an interface/trait
|
||||
M.implement_interface = [[Implement the following interface/trait:
|
||||
|
||||
{{description}}
|
||||
|
||||
Requirements:
|
||||
- Implement all required methods
|
||||
- Follow the interface contract exactly
|
||||
- Handle edge cases appropriately
|
||||
]]
|
||||
|
||||
--- Prompt template for creating a React component
|
||||
M.create_react_component = [[Create a React component with the following requirements:
|
||||
|
||||
{{description}}
|
||||
|
||||
Requirements:
|
||||
- Use functional components with hooks
|
||||
- Include proper TypeScript types (if .tsx)
|
||||
- Follow React best practices
|
||||
- Make it reusable and composable
|
||||
]]
|
||||
|
||||
--- Prompt template for creating an API endpoint
|
||||
M.create_api_endpoint = [[Create an API endpoint with the following requirements:
|
||||
|
||||
{{description}}
|
||||
|
||||
Requirements:
|
||||
- Include input validation
|
||||
- Proper error handling and status codes
|
||||
- Follow RESTful conventions
|
||||
- Include appropriate middleware
|
||||
]]
|
||||
|
||||
--- Prompt template for creating a utility function
|
||||
M.create_utility = [[Create a utility function:
|
||||
|
||||
{{description}}
|
||||
|
||||
Requirements:
|
||||
- Pure function (no side effects) if possible
|
||||
- Handle edge cases
|
||||
- Efficient implementation
|
||||
- Well-typed (if applicable)
|
||||
]]
|
||||
|
||||
--- Prompt template for generic code generation
|
||||
M.generic = [[Generate code based on the following description:
|
||||
|
||||
{{description}}
|
||||
|
||||
Context:
|
||||
- Language: {{language}}
|
||||
- File: {{filepath}}
|
||||
|
||||
Requirements:
|
||||
- Match existing code style
|
||||
- Follow best practices
|
||||
- Handle errors appropriately
|
||||
]]
|
||||
|
||||
return M
|
||||
136
lua/codetyper/prompts/document.lua
Normal file
136
lua/codetyper/prompts/document.lua
Normal file
@@ -0,0 +1,136 @@
|
||||
---@mod codetyper.prompts.document Documentation prompts for Codetyper.nvim
|
||||
---
|
||||
--- These prompts are used for generating documentation.
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Prompt for adding JSDoc comments
|
||||
M.jsdoc = [[Add JSDoc documentation to this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Requirements:
|
||||
- Document all functions and methods
|
||||
- Include @param for all parameters
|
||||
- Include @returns for return values
|
||||
- Add @throws if exceptions are thrown
|
||||
- Include @example where helpful
|
||||
- Use @typedef for complex types
|
||||
]]
|
||||
|
||||
--- Prompt for adding Python docstrings
|
||||
M.python_docstring = [[Add docstrings to this Python code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Requirements:
|
||||
- Use Google-style docstrings
|
||||
- Document all functions and classes
|
||||
- Include Args, Returns, Raises sections
|
||||
- Add Examples where helpful
|
||||
- Include type hints in docstrings
|
||||
]]
|
||||
|
||||
--- Prompt for adding LuaDoc comments
|
||||
M.luadoc = [[Add LuaDoc/EmmyLua annotations to this Lua code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Requirements:
|
||||
- Use ---@param for parameters
|
||||
- Use ---@return for return values
|
||||
- Use ---@class for table structures
|
||||
- Use ---@field for class fields
|
||||
- Add descriptions for all items
|
||||
]]
|
||||
|
||||
--- Prompt for adding Go documentation
|
||||
M.godoc = [[Add GoDoc comments to this Go code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Requirements:
|
||||
- Start comments with the name being documented
|
||||
- Document all exported functions, types, and variables
|
||||
- Keep comments concise but complete
|
||||
- Follow Go documentation conventions
|
||||
]]
|
||||
|
||||
--- Prompt for adding README documentation
|
||||
M.readme = [[Generate README documentation for this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Include:
|
||||
- Project description
|
||||
- Installation instructions
|
||||
- Usage examples
|
||||
- API documentation
|
||||
- Contributing guidelines
|
||||
]]
|
||||
|
||||
--- Prompt for adding inline comments
|
||||
M.inline_comments = [[Add helpful inline comments to this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Guidelines:
|
||||
- Explain complex logic
|
||||
- Document non-obvious decisions
|
||||
- Don't state the obvious
|
||||
- Keep comments concise
|
||||
- Use TODO/FIXME where appropriate
|
||||
]]
|
||||
|
||||
--- Prompt for adding API documentation
|
||||
M.api_docs = [[Generate API documentation for this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Include for each endpoint/function:
|
||||
- Description
|
||||
- Parameters with types
|
||||
- Return value with type
|
||||
- Example request/response
|
||||
- Error cases
|
||||
]]
|
||||
|
||||
--- Prompt for adding type definitions
|
||||
M.type_definitions = [[Generate type definitions for this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Requirements:
|
||||
- Define interfaces/types for all data structures
|
||||
- Include optional properties where appropriate
|
||||
- Add JSDoc/docstring descriptions
|
||||
- Export all types that should be public
|
||||
]]
|
||||
|
||||
--- Prompt for changelog entry
|
||||
M.changelog = [[Generate a changelog entry for these changes:
|
||||
|
||||
{{changes}}
|
||||
|
||||
Format:
|
||||
- Use conventional changelog format
|
||||
- Categorize as Added/Changed/Fixed/Removed
|
||||
- Be concise but descriptive
|
||||
- Include breaking changes prominently
|
||||
]]
|
||||
|
||||
--- Generic documentation prompt
|
||||
M.generic = [[Add documentation to this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Language: {{language}}
|
||||
|
||||
Requirements:
|
||||
- Use appropriate documentation format for the language
|
||||
- Document all public APIs
|
||||
- Include parameter and return descriptions
|
||||
- Add examples where helpful
|
||||
]]
|
||||
|
||||
return M
|
||||
56
lua/codetyper/prompts/init.lua
Normal file
56
lua/codetyper/prompts/init.lua
Normal file
@@ -0,0 +1,56 @@
|
||||
---@mod codetyper.prompts Prompt templates for Codetyper.nvim
|
||||
---
|
||||
--- This module provides all prompt templates used by the plugin.
|
||||
--- Prompts are organized by functionality and can be customized.
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Load all prompt modules
|
||||
M.system = require("codetyper.prompts.system")
|
||||
M.code = require("codetyper.prompts.code")
|
||||
M.ask = require("codetyper.prompts.ask")
|
||||
M.refactor = require("codetyper.prompts.refactor")
|
||||
M.document = require("codetyper.prompts.document")
|
||||
|
||||
--- Get a prompt by category and name
|
||||
---@param category string Category name (system, code, ask, refactor, document)
|
||||
---@param name string Prompt name
|
||||
---@param vars? table Variables to substitute in the prompt
|
||||
---@return string Formatted prompt
|
||||
function M.get(category, name, vars)
|
||||
local prompts = M[category]
|
||||
if not prompts then
|
||||
error("Unknown prompt category: " .. category)
|
||||
end
|
||||
|
||||
local prompt = prompts[name]
|
||||
if not prompt then
|
||||
error("Unknown prompt: " .. category .. "." .. name)
|
||||
end
|
||||
|
||||
-- Substitute variables if provided
|
||||
if vars then
|
||||
for key, value in pairs(vars) do
|
||||
prompt = prompt:gsub("{{" .. key .. "}}", tostring(value))
|
||||
end
|
||||
end
|
||||
|
||||
return prompt
|
||||
end
|
||||
|
||||
--- List all available prompts
|
||||
---@return table Available prompts by category
|
||||
function M.list()
|
||||
local result = {}
|
||||
for category, prompts in pairs(M) do
|
||||
if type(prompts) == "table" and category ~= "list" and category ~= "get" then
|
||||
result[category] = {}
|
||||
for name, _ in pairs(prompts) do
|
||||
table.insert(result[category], name)
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
return M
|
||||
128
lua/codetyper/prompts/refactor.lua
Normal file
128
lua/codetyper/prompts/refactor.lua
Normal file
@@ -0,0 +1,128 @@
|
||||
---@mod codetyper.prompts.refactor Refactoring prompts for Codetyper.nvim
|
||||
---
|
||||
--- These prompts are used for code refactoring operations.
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Prompt for general refactoring
|
||||
M.general = [[Refactor this code to improve its quality:
|
||||
|
||||
{{code}}
|
||||
|
||||
Focus on:
|
||||
- Readability
|
||||
- Maintainability
|
||||
- Following best practices
|
||||
- Keeping the same functionality
|
||||
]]
|
||||
|
||||
--- Prompt for extracting a function
|
||||
M.extract_function = [[Extract a function from this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
The function should:
|
||||
{{description}}
|
||||
|
||||
Requirements:
|
||||
- Give it a meaningful name
|
||||
- Include proper parameters
|
||||
- Return appropriate values
|
||||
]]
|
||||
|
||||
--- Prompt for simplifying code
|
||||
M.simplify = [[Simplify this code while maintaining functionality:
|
||||
|
||||
{{code}}
|
||||
|
||||
Goals:
|
||||
- Reduce complexity
|
||||
- Remove redundancy
|
||||
- Improve readability
|
||||
- Keep all existing behavior
|
||||
]]
|
||||
|
||||
--- Prompt for converting to async/await
|
||||
M.async_await = [[Convert this code to use async/await:
|
||||
|
||||
{{code}}
|
||||
|
||||
Requirements:
|
||||
- Convert all promises to async/await
|
||||
- Maintain error handling
|
||||
- Keep the same functionality
|
||||
]]
|
||||
|
||||
--- Prompt for adding error handling
|
||||
M.add_error_handling = [[Add proper error handling to this code:
|
||||
|
||||
{{code}}
|
||||
|
||||
Requirements:
|
||||
- Handle all potential errors
|
||||
- Use appropriate error types
|
||||
- Add meaningful error messages
|
||||
- Don't change core functionality
|
||||
]]
|
||||
|
||||
--- Prompt for improving performance
|
||||
M.optimize_performance = [[Optimize this code for better performance:
|
||||
|
||||
{{code}}
|
||||
|
||||
Focus on:
|
||||
- Algorithm efficiency
|
||||
- Memory usage
|
||||
- Reducing unnecessary operations
|
||||
- Maintaining readability
|
||||
]]
|
||||
|
||||
--- Prompt for converting to TypeScript
|
||||
M.convert_to_typescript = [[Convert this JavaScript code to TypeScript:
|
||||
|
||||
{{code}}
|
||||
|
||||
Requirements:
|
||||
- Add proper type annotations
|
||||
- Use interfaces where appropriate
|
||||
- Handle null/undefined properly
|
||||
- Maintain all functionality
|
||||
]]
|
||||
|
||||
--- Prompt for applying design pattern
|
||||
M.apply_pattern = [[Refactor this code to use the {{pattern}} pattern:
|
||||
|
||||
{{code}}
|
||||
|
||||
Requirements:
|
||||
- Properly implement the pattern
|
||||
- Maintain existing functionality
|
||||
- Improve code organization
|
||||
]]
|
||||
|
||||
--- Prompt for splitting a large function
|
||||
M.split_function = [[Split this large function into smaller, focused functions:
|
||||
|
||||
{{code}}
|
||||
|
||||
Goals:
|
||||
- Single responsibility per function
|
||||
- Clear function names
|
||||
- Proper parameter passing
|
||||
- Maintain all functionality
|
||||
]]
|
||||
|
||||
--- Prompt for removing code smells
|
||||
M.remove_code_smells = [[Refactor this code to remove code smells:
|
||||
|
||||
{{code}}
|
||||
|
||||
Look for and fix:
|
||||
- Long methods
|
||||
- Duplicated code
|
||||
- Magic numbers
|
||||
- Deep nesting
|
||||
- Other anti-patterns
|
||||
]]
|
||||
|
||||
return M
|
||||
96
lua/codetyper/prompts/system.lua
Normal file
96
lua/codetyper/prompts/system.lua
Normal file
@@ -0,0 +1,96 @@
|
||||
---@mod codetyper.prompts.system System prompts for Codetyper.nvim
|
||||
---
|
||||
--- These are the base system prompts that define the AI's behavior.
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Base system prompt for code generation
|
||||
M.code_generation = [[You are an expert code generation assistant integrated into Neovim via Codetyper.nvim.
|
||||
Your task is to generate high-quality, production-ready code based on the user's prompt.
|
||||
|
||||
CRITICAL RULES:
|
||||
1. Output ONLY the code - no explanations, no markdown code blocks, no comments about what you did
|
||||
2. Match the coding style, conventions, and patterns of the existing file
|
||||
3. Use proper indentation and formatting for the language
|
||||
4. Follow best practices for the specific language/framework
|
||||
5. Preserve existing functionality unless explicitly asked to change it
|
||||
6. Use meaningful variable and function names
|
||||
7. Handle edge cases and errors appropriately
|
||||
|
||||
Language: {{language}}
|
||||
File: {{filepath}}
|
||||
]]
|
||||
|
||||
--- System prompt for code explanation/ask
|
||||
M.ask = [[You are a helpful coding assistant integrated into Neovim via Codetyper.nvim.
|
||||
You help developers understand code, explain concepts, and answer programming questions.
|
||||
|
||||
GUIDELINES:
|
||||
1. Be concise but thorough in your explanations
|
||||
2. Use code examples when helpful
|
||||
3. Reference the provided code context in your explanations
|
||||
4. Format responses in markdown for readability
|
||||
5. If you don't know something, say so honestly
|
||||
6. Break down complex concepts into understandable parts
|
||||
7. Provide practical, actionable advice
|
||||
|
||||
IMPORTANT: When file contents are provided, analyze them carefully and base your response on the actual code.
|
||||
]]
|
||||
|
||||
--- System prompt for refactoring
|
||||
M.refactor = [[You are an expert code refactoring assistant integrated into Neovim via Codetyper.nvim.
|
||||
Your task is to refactor code while maintaining its functionality.
|
||||
|
||||
CRITICAL RULES:
|
||||
1. Output ONLY the refactored code - no explanations
|
||||
2. Preserve ALL existing functionality
|
||||
3. Improve code quality, readability, and maintainability
|
||||
4. Follow SOLID principles and best practices
|
||||
5. Keep the same coding style as the original
|
||||
6. Do not add new features unless explicitly requested
|
||||
7. Optimize performance where possible without sacrificing readability
|
||||
|
||||
Language: {{language}}
|
||||
]]
|
||||
|
||||
--- System prompt for documentation
|
||||
M.document = [[You are a documentation expert integrated into Neovim via Codetyper.nvim.
|
||||
Your task is to generate clear, comprehensive documentation for code.
|
||||
|
||||
CRITICAL RULES:
|
||||
1. Output ONLY the documentation/comments - ready to be inserted into code
|
||||
2. Use the appropriate documentation format for the language:
|
||||
- JavaScript/TypeScript: JSDoc
|
||||
- Python: Docstrings (Google or NumPy style)
|
||||
- Lua: LuaDoc/EmmyLua
|
||||
- Go: GoDoc
|
||||
- Rust: RustDoc
|
||||
- Java: Javadoc
|
||||
3. Document all parameters, return values, and exceptions
|
||||
4. Include usage examples where helpful
|
||||
5. Be concise but complete
|
||||
|
||||
Language: {{language}}
|
||||
]]
|
||||
|
||||
--- System prompt for test generation
|
||||
M.test = [[You are a test generation expert integrated into Neovim via Codetyper.nvim.
|
||||
Your task is to generate comprehensive unit tests for the provided code.
|
||||
|
||||
CRITICAL RULES:
|
||||
1. Output ONLY the test code - no explanations
|
||||
2. Use the appropriate testing framework for the language:
|
||||
- JavaScript/TypeScript: Jest or Vitest
|
||||
- Python: pytest
|
||||
- Lua: busted or plenary
|
||||
- Go: testing package
|
||||
- Rust: built-in tests
|
||||
3. Cover happy paths, edge cases, and error scenarios
|
||||
4. Use descriptive test names
|
||||
5. Follow AAA pattern: Arrange, Act, Assert
|
||||
6. Mock external dependencies appropriately
|
||||
|
||||
Language: {{language}}
|
||||
]]
|
||||
|
||||
return M
|
||||
245
lua/codetyper/tree.lua
Normal file
245
lua/codetyper/tree.lua
Normal file
@@ -0,0 +1,245 @@
|
||||
---@mod codetyper.tree Project tree logging for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
|
||||
--- Name of the coder folder
|
||||
local CODER_FOLDER = ".coder"
|
||||
|
||||
--- Name of the tree log file
|
||||
local TREE_LOG_FILE = "tree.log"
|
||||
|
||||
--- Get the path to the .coder folder
|
||||
---@return string|nil Path to .coder folder or nil
|
||||
function M.get_coder_folder()
|
||||
local root = utils.get_project_root()
|
||||
if not root then
|
||||
return nil
|
||||
end
|
||||
return root .. "/" .. CODER_FOLDER
|
||||
end
|
||||
|
||||
--- Get the path to the tree.log file
|
||||
---@return string|nil Path to tree.log or nil
|
||||
function M.get_tree_log_path()
|
||||
local coder_folder = M.get_coder_folder()
|
||||
if not coder_folder then
|
||||
return nil
|
||||
end
|
||||
return coder_folder .. "/" .. TREE_LOG_FILE
|
||||
end
|
||||
|
||||
--- Ensure .coder folder exists
|
||||
---@return boolean Success status
|
||||
function M.ensure_coder_folder()
|
||||
local coder_folder = M.get_coder_folder()
|
||||
if not coder_folder then
|
||||
return false
|
||||
end
|
||||
return utils.ensure_dir(coder_folder)
|
||||
end
|
||||
|
||||
--- Build tree structure recursively
|
||||
---@param path string Directory path
|
||||
---@param prefix string Prefix for tree lines
|
||||
---@param ignore_patterns table Patterns to ignore
|
||||
---@return string[] Tree lines
|
||||
local function build_tree(path, prefix, ignore_patterns)
|
||||
local lines = {}
|
||||
local entries = {}
|
||||
|
||||
-- Get directory entries
|
||||
local handle = vim.loop.fs_scandir(path)
|
||||
if not handle then
|
||||
return lines
|
||||
end
|
||||
|
||||
while true do
|
||||
local name, type = vim.loop.fs_scandir_next(handle)
|
||||
if not name then
|
||||
break
|
||||
end
|
||||
|
||||
-- Check if should be ignored
|
||||
local should_ignore = false
|
||||
for _, pattern in ipairs(ignore_patterns) do
|
||||
if name:match(pattern) then
|
||||
should_ignore = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not should_ignore then
|
||||
table.insert(entries, { name = name, type = type })
|
||||
end
|
||||
end
|
||||
|
||||
-- Sort entries (directories first, then alphabetically)
|
||||
table.sort(entries, function(a, b)
|
||||
if a.type == "directory" and b.type ~= "directory" then
|
||||
return true
|
||||
elseif a.type ~= "directory" and b.type == "directory" then
|
||||
return false
|
||||
end
|
||||
return a.name < b.name
|
||||
end)
|
||||
|
||||
-- Build tree lines
|
||||
for i, entry in ipairs(entries) do
|
||||
local is_last = i == #entries
|
||||
local connector = is_last and "└── " or "├── "
|
||||
local child_prefix = is_last and " " or "│ "
|
||||
|
||||
local icon = ""
|
||||
if entry.type == "directory" then
|
||||
icon = "📁 "
|
||||
else
|
||||
-- File type icons
|
||||
local ext = entry.name:match("%.([^%.]+)$")
|
||||
local icons = {
|
||||
lua = "🌙 ",
|
||||
ts = "📘 ",
|
||||
tsx = "⚛️ ",
|
||||
js = "📒 ",
|
||||
jsx = "⚛️ ",
|
||||
py = "🐍 ",
|
||||
go = "🐹 ",
|
||||
rs = "🦀 ",
|
||||
md = "📝 ",
|
||||
json = "📋 ",
|
||||
yaml = "📋 ",
|
||||
yml = "📋 ",
|
||||
html = "🌐 ",
|
||||
css = "🎨 ",
|
||||
scss = "🎨 ",
|
||||
}
|
||||
icon = icons[ext] or "📄 "
|
||||
end
|
||||
|
||||
table.insert(lines, prefix .. connector .. icon .. entry.name)
|
||||
|
||||
if entry.type == "directory" then
|
||||
local child_path = path .. "/" .. entry.name
|
||||
local child_lines = build_tree(child_path, prefix .. child_prefix, ignore_patterns)
|
||||
for _, line in ipairs(child_lines) do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return lines
|
||||
end
|
||||
|
||||
--- Generate project tree
|
||||
---@return string Tree content
|
||||
function M.generate_tree()
|
||||
local root = utils.get_project_root()
|
||||
if not root then
|
||||
return "-- Could not determine project root --"
|
||||
end
|
||||
|
||||
local project_name = vim.fn.fnamemodify(root, ":t")
|
||||
local timestamp = os.date("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
-- Patterns to ignore
|
||||
local ignore_patterns = {
|
||||
"^%.", -- Hidden files/folders
|
||||
"^node_modules$",
|
||||
"^__pycache__$",
|
||||
"^%.git$",
|
||||
"^%.coder$",
|
||||
"^dist$",
|
||||
"^build$",
|
||||
"^target$",
|
||||
"^vendor$",
|
||||
"%.coder%.", -- Coder files
|
||||
}
|
||||
|
||||
local lines = {
|
||||
"# Project Tree: " .. project_name,
|
||||
"# Generated: " .. timestamp,
|
||||
"# By: Codetyper.nvim",
|
||||
"",
|
||||
"📦 " .. project_name,
|
||||
}
|
||||
|
||||
local tree_lines = build_tree(root, "", ignore_patterns)
|
||||
for _, line in ipairs(tree_lines) do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, "# Total files tracked by Codetyper")
|
||||
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
--- Update the tree.log file
|
||||
---@return boolean Success status
|
||||
function M.update_tree_log()
|
||||
-- Ensure .coder folder exists
|
||||
if not M.ensure_coder_folder() then
|
||||
return false
|
||||
end
|
||||
|
||||
local tree_log_path = M.get_tree_log_path()
|
||||
if not tree_log_path then
|
||||
return false
|
||||
end
|
||||
|
||||
local tree_content = M.generate_tree()
|
||||
|
||||
if utils.write_file(tree_log_path, tree_content) then
|
||||
-- Silent update, no notification needed for every file change
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--- Initialize tree logging (called on setup)
|
||||
function M.setup()
|
||||
-- Create initial tree log
|
||||
M.update_tree_log()
|
||||
end
|
||||
|
||||
--- Get file statistics from tree
|
||||
---@return table Statistics { files: number, directories: number }
|
||||
function M.get_stats()
|
||||
local root = utils.get_project_root()
|
||||
if not root then
|
||||
return { files = 0, directories = 0 }
|
||||
end
|
||||
|
||||
local stats = { files = 0, directories = 0 }
|
||||
|
||||
local function count_recursive(path)
|
||||
local handle = vim.loop.fs_scandir(path)
|
||||
if not handle then
|
||||
return
|
||||
end
|
||||
|
||||
while true do
|
||||
local name, type = vim.loop.fs_scandir_next(handle)
|
||||
if not name then
|
||||
break
|
||||
end
|
||||
|
||||
-- Skip hidden and special folders
|
||||
if not name:match("^%.") and name ~= "node_modules" and not name:match("%.coder%.") then
|
||||
if type == "directory" then
|
||||
stats.directories = stats.directories + 1
|
||||
count_recursive(path .. "/" .. name)
|
||||
else
|
||||
stats.files = stats.files + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
count_recursive(root)
|
||||
return stats
|
||||
end
|
||||
|
||||
return M
|
||||
44
lua/codetyper/types.lua
Normal file
44
lua/codetyper/types.lua
Normal file
@@ -0,0 +1,44 @@
|
||||
---@mod codetyper.types Type definitions for Codetyper.nvim
|
||||
|
||||
---@class CoderConfig
|
||||
---@field llm LLMConfig LLM provider configuration
|
||||
---@field window WindowConfig Window configuration
|
||||
---@field patterns PatternConfig Pattern configuration
|
||||
---@field auto_gitignore boolean Auto-manage .gitignore
|
||||
|
||||
---@class LLMConfig
|
||||
---@field provider "claude" | "ollama" The LLM provider to use
|
||||
---@field claude ClaudeConfig Claude-specific configuration
|
||||
---@field ollama OllamaConfig Ollama-specific configuration
|
||||
|
||||
---@class ClaudeConfig
|
||||
---@field api_key string | nil Claude API key (or env var ANTHROPIC_API_KEY)
|
||||
---@field model string Claude model to use
|
||||
|
||||
---@class OllamaConfig
|
||||
---@field host string Ollama host URL
|
||||
---@field model string Ollama model to use
|
||||
|
||||
---@class WindowConfig
|
||||
---@field width number Width of the coder window (percentage or columns)
|
||||
---@field position "left" | "right" Position of the coder window
|
||||
---@field border string Border style for floating windows
|
||||
|
||||
---@class PatternConfig
|
||||
---@field open_tag string Opening tag for prompts
|
||||
---@field close_tag string Closing tag for prompts
|
||||
---@field file_pattern string Pattern for coder files
|
||||
|
||||
---@class CoderPrompt
|
||||
---@field content string The prompt content between tags
|
||||
---@field start_line number Starting line number
|
||||
---@field end_line number Ending line number
|
||||
---@field start_col number Starting column
|
||||
---@field end_col number Ending column
|
||||
|
||||
---@class CoderFile
|
||||
---@field coder_path string Path to the .coder.* file
|
||||
---@field target_path string Path to the target file
|
||||
---@field filetype string The filetype/extension
|
||||
|
||||
return {}
|
||||
125
lua/codetyper/utils.lua
Normal file
125
lua/codetyper/utils.lua
Normal file
@@ -0,0 +1,125 @@
|
||||
---@mod codetyper.utils Utility functions for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Get the project root directory
|
||||
---@return string|nil Root directory path or nil if not found
|
||||
function M.get_project_root()
|
||||
-- Try to find common root indicators
|
||||
local markers = { ".git", ".gitignore", "package.json", "Cargo.toml", "go.mod", "pyproject.toml" }
|
||||
|
||||
local current = vim.fn.getcwd()
|
||||
|
||||
for _, marker in ipairs(markers) do
|
||||
local found = vim.fn.findfile(marker, current .. ";")
|
||||
if found ~= "" then
|
||||
return vim.fn.fnamemodify(found, ":p:h")
|
||||
end
|
||||
found = vim.fn.finddir(marker, current .. ";")
|
||||
if found ~= "" then
|
||||
return vim.fn.fnamemodify(found, ":p:h")
|
||||
end
|
||||
end
|
||||
|
||||
return current
|
||||
end
|
||||
|
||||
--- Check if a file is a coder file
|
||||
---@param filepath string File path to check
|
||||
---@return boolean
|
||||
function M.is_coder_file(filepath)
|
||||
return filepath:match("%.coder%.") ~= nil
|
||||
end
|
||||
|
||||
--- Get the target file path from a coder file path
|
||||
---@param coder_path string Path to the coder file
|
||||
---@return string Target file path
|
||||
function M.get_target_path(coder_path)
|
||||
-- Convert index.coder.ts -> index.ts
|
||||
return coder_path:gsub("%.coder%.", ".")
|
||||
end
|
||||
|
||||
--- Get the coder file path from a target file path
|
||||
---@param target_path string Path to the target file
|
||||
---@return string Coder file path
|
||||
function M.get_coder_path(target_path)
|
||||
-- Convert index.ts -> index.coder.ts
|
||||
local dir = vim.fn.fnamemodify(target_path, ":h")
|
||||
local name = vim.fn.fnamemodify(target_path, ":t:r")
|
||||
local ext = vim.fn.fnamemodify(target_path, ":e")
|
||||
|
||||
if dir == "." then
|
||||
return name .. ".coder." .. ext
|
||||
end
|
||||
return dir .. "/" .. name .. ".coder." .. ext
|
||||
end
|
||||
|
||||
--- Check if a file exists
|
||||
---@param filepath string File path to check
|
||||
---@return boolean
|
||||
function M.file_exists(filepath)
|
||||
local stat = vim.loop.fs_stat(filepath)
|
||||
return stat ~= nil
|
||||
end
|
||||
|
||||
--- Read file contents
|
||||
---@param filepath string File path to read
|
||||
---@return string|nil Contents or nil if error
|
||||
function M.read_file(filepath)
|
||||
local file = io.open(filepath, "r")
|
||||
if not file then
|
||||
return nil
|
||||
end
|
||||
local content = file:read("*all")
|
||||
file:close()
|
||||
return content
|
||||
end
|
||||
|
||||
--- Write content to file
|
||||
---@param filepath string File path to write
|
||||
---@param content string Content to write
|
||||
---@return boolean Success status
|
||||
function M.write_file(filepath, content)
|
||||
local file = io.open(filepath, "w")
|
||||
if not file then
|
||||
return false
|
||||
end
|
||||
file:write(content)
|
||||
file:close()
|
||||
return true
|
||||
end
|
||||
|
||||
--- Create directory if it doesn't exist
|
||||
---@param dirpath string Directory path
|
||||
---@return boolean Success status
|
||||
function M.ensure_dir(dirpath)
|
||||
if vim.fn.isdirectory(dirpath) == 0 then
|
||||
return vim.fn.mkdir(dirpath, "p") == 1
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Notify user with proper formatting
|
||||
---@param msg string Message to display
|
||||
---@param level? number Vim log level (default: INFO)
|
||||
function M.notify(msg, level)
|
||||
level = level or vim.log.levels.INFO
|
||||
vim.notify("[Codetyper] " .. msg, level)
|
||||
end
|
||||
|
||||
--- Get buffer filetype
|
||||
---@param bufnr? number Buffer number (default: current)
|
||||
---@return string Filetype
|
||||
function M.get_filetype(bufnr)
|
||||
bufnr = bufnr or 0
|
||||
return vim.bo[bufnr].filetype
|
||||
end
|
||||
|
||||
--- Escape pattern special characters
|
||||
---@param str string String to escape
|
||||
---@return string Escaped string
|
||||
function M.escape_pattern(str)
|
||||
return str:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1")
|
||||
end
|
||||
|
||||
return M
|
||||
177
lua/codetyper/window.lua
Normal file
177
lua/codetyper/window.lua
Normal file
@@ -0,0 +1,177 @@
|
||||
---@mod codetyper.window Window management for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.utils")
|
||||
|
||||
---@type number|nil Current coder window ID
|
||||
M._coder_win = nil
|
||||
|
||||
---@type number|nil Current target window ID
|
||||
M._target_win = nil
|
||||
|
||||
---@type number|nil Current coder buffer ID
|
||||
M._coder_buf = nil
|
||||
|
||||
---@type number|nil Current target buffer ID
|
||||
M._target_buf = nil
|
||||
|
||||
--- Calculate window width based on configuration
|
||||
---@param config CoderConfig Plugin configuration
|
||||
---@return number Width in columns
|
||||
local function calculate_width(config)
|
||||
local width = config.window.width
|
||||
if width <= 1 then
|
||||
-- Percentage of total width
|
||||
return math.floor(vim.o.columns * width)
|
||||
end
|
||||
return math.floor(width)
|
||||
end
|
||||
|
||||
--- Open the coder split view
|
||||
---@param target_path string Path to the target file
|
||||
---@param coder_path string Path to the coder file
|
||||
---@return boolean Success status
|
||||
function M.open_split(target_path, coder_path)
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
|
||||
-- Ensure coder file exists, create if not
|
||||
if not utils.file_exists(coder_path) then
|
||||
local dir = vim.fn.fnamemodify(coder_path, ":h")
|
||||
utils.ensure_dir(dir)
|
||||
utils.write_file(coder_path, "")
|
||||
|
||||
-- Ensure gitignore is updated when creating a new coder file
|
||||
local gitignore = require("codetyper.gitignore")
|
||||
gitignore.ensure_ignored()
|
||||
end
|
||||
|
||||
-- Store current window as target window
|
||||
M._target_win = vim.api.nvim_get_current_win()
|
||||
M._target_buf = vim.api.nvim_get_current_buf()
|
||||
|
||||
-- Open target file if not already open
|
||||
if vim.fn.expand("%:p") ~= target_path then
|
||||
vim.cmd("edit " .. vim.fn.fnameescape(target_path))
|
||||
M._target_buf = vim.api.nvim_get_current_buf()
|
||||
end
|
||||
|
||||
-- Calculate width
|
||||
local width = calculate_width(config)
|
||||
|
||||
-- Create the coder split
|
||||
if config.window.position == "left" then
|
||||
vim.cmd("topleft vsplit " .. vim.fn.fnameescape(coder_path))
|
||||
else
|
||||
vim.cmd("botright vsplit " .. vim.fn.fnameescape(coder_path))
|
||||
end
|
||||
|
||||
-- Store coder window reference
|
||||
M._coder_win = vim.api.nvim_get_current_win()
|
||||
M._coder_buf = vim.api.nvim_get_current_buf()
|
||||
|
||||
-- Set coder window width
|
||||
vim.api.nvim_win_set_width(M._coder_win, width)
|
||||
|
||||
-- Set up window options for coder window
|
||||
vim.wo[M._coder_win].number = true
|
||||
vim.wo[M._coder_win].relativenumber = true
|
||||
vim.wo[M._coder_win].wrap = true
|
||||
vim.wo[M._coder_win].signcolumn = "yes"
|
||||
|
||||
-- Focus on target window (right side) by default
|
||||
if config.window.position == "left" then
|
||||
vim.api.nvim_set_current_win(M._target_win)
|
||||
end
|
||||
|
||||
utils.notify("Coder view opened: " .. vim.fn.fnamemodify(coder_path, ":t"))
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- Close the coder split view
|
||||
---@return boolean Success status
|
||||
function M.close_split()
|
||||
if M._coder_win and vim.api.nvim_win_is_valid(M._coder_win) then
|
||||
vim.api.nvim_win_close(M._coder_win, false)
|
||||
M._coder_win = nil
|
||||
M._coder_buf = nil
|
||||
utils.notify("Coder view closed")
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- Toggle the coder split view
|
||||
---@param target_path? string Path to the target file
|
||||
---@param coder_path? string Path to the coder file
|
||||
function M.toggle_split(target_path, coder_path)
|
||||
if M._coder_win and vim.api.nvim_win_is_valid(M._coder_win) then
|
||||
M.close_split()
|
||||
else
|
||||
if target_path and coder_path then
|
||||
M.open_split(target_path, coder_path)
|
||||
else
|
||||
utils.notify("No file specified for coder view", vim.log.levels.WARN)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Check if coder view is currently open
|
||||
---@return boolean
|
||||
function M.is_open()
|
||||
return M._coder_win ~= nil and vim.api.nvim_win_is_valid(M._coder_win)
|
||||
end
|
||||
|
||||
--- Get current coder window ID
|
||||
---@return number|nil
|
||||
function M.get_coder_win()
|
||||
return M._coder_win
|
||||
end
|
||||
|
||||
--- Get current target window ID
|
||||
---@return number|nil
|
||||
function M.get_target_win()
|
||||
return M._target_win
|
||||
end
|
||||
|
||||
--- Get current coder buffer ID
|
||||
---@return number|nil
|
||||
function M.get_coder_buf()
|
||||
return M._coder_buf
|
||||
end
|
||||
|
||||
--- Get current target buffer ID
|
||||
---@return number|nil
|
||||
function M.get_target_buf()
|
||||
return M._target_buf
|
||||
end
|
||||
|
||||
--- Focus on the coder window
|
||||
function M.focus_coder()
|
||||
if M._coder_win and vim.api.nvim_win_is_valid(M._coder_win) then
|
||||
vim.api.nvim_set_current_win(M._coder_win)
|
||||
end
|
||||
end
|
||||
|
||||
--- Focus on the target window
|
||||
function M.focus_target()
|
||||
if M._target_win and vim.api.nvim_win_is_valid(M._target_win) then
|
||||
vim.api.nvim_set_current_win(M._target_win)
|
||||
end
|
||||
end
|
||||
|
||||
--- Sync scroll between windows (optional feature)
|
||||
---@param enable boolean Enable or disable sync scroll
|
||||
function M.sync_scroll(enable)
|
||||
if not M.is_open() then
|
||||
return
|
||||
end
|
||||
|
||||
local value = enable and "scrollbind" or "noscrollbind"
|
||||
vim.wo[M._coder_win][value] = enable
|
||||
vim.wo[M._target_win][value] = enable
|
||||
end
|
||||
|
||||
return M
|
||||
121
plugin/codetyper.lua
Normal file
121
plugin/codetyper.lua
Normal file
@@ -0,0 +1,121 @@
|
||||
-- Codetyper.nvim - AI-powered coding partner for Neovim
|
||||
-- Plugin loader
|
||||
|
||||
-- Prevent loading twice
|
||||
if vim.g.loaded_codetyper then
|
||||
return
|
||||
end
|
||||
vim.g.loaded_codetyper = true
|
||||
|
||||
-- Minimum Neovim version check
|
||||
if vim.fn.has("nvim-0.8.0") == 0 then
|
||||
vim.api.nvim_err_writeln("Codetyper.nvim requires Neovim 0.8.0 or higher")
|
||||
return
|
||||
end
|
||||
|
||||
-- Initialize .coder folder and tree.log on project open
|
||||
vim.api.nvim_create_autocmd("VimEnter", {
|
||||
callback = function()
|
||||
-- Delay slightly to ensure cwd is set
|
||||
vim.defer_fn(function()
|
||||
local tree = require("codetyper.tree")
|
||||
tree.setup()
|
||||
|
||||
-- Also ensure gitignore is updated
|
||||
local gitignore = require("codetyper.gitignore")
|
||||
gitignore.ensure_ignored()
|
||||
end, 100)
|
||||
end,
|
||||
desc = "Initialize Codetyper .coder folder on startup",
|
||||
})
|
||||
|
||||
-- Also initialize on directory change
|
||||
vim.api.nvim_create_autocmd("DirChanged", {
|
||||
callback = function()
|
||||
vim.defer_fn(function()
|
||||
local tree = require("codetyper.tree")
|
||||
tree.setup()
|
||||
|
||||
local gitignore = require("codetyper.gitignore")
|
||||
gitignore.ensure_ignored()
|
||||
end, 100)
|
||||
end,
|
||||
desc = "Initialize Codetyper .coder folder on directory change",
|
||||
})
|
||||
|
||||
-- Auto-initialize when opening a coder file (for nvim-tree, telescope, etc.)
|
||||
vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile", "BufEnter" }, {
|
||||
pattern = "*.coder.*",
|
||||
callback = function()
|
||||
-- Initialize plugin if not already done
|
||||
local codetyper = require("codetyper")
|
||||
if not codetyper.is_initialized() then
|
||||
codetyper.setup()
|
||||
end
|
||||
end,
|
||||
desc = "Auto-initialize Codetyper when opening coder files",
|
||||
})
|
||||
|
||||
-- Lazy-load the plugin on first command usage
|
||||
vim.api.nvim_create_user_command("Coder", function(opts)
|
||||
require("codetyper").setup()
|
||||
-- Re-execute the command now that plugin is loaded
|
||||
vim.cmd("Coder " .. (opts.args or ""))
|
||||
end, {
|
||||
nargs = "?",
|
||||
complete = function()
|
||||
return {
|
||||
"open", "close", "toggle", "process", "status", "focus",
|
||||
"tree", "tree-view", "reset", "gitignore",
|
||||
"ask", "ask-close", "ask-toggle", "ask-clear",
|
||||
}
|
||||
end,
|
||||
desc = "Codetyper.nvim commands",
|
||||
})
|
||||
|
||||
-- Lazy-load aliases
|
||||
vim.api.nvim_create_user_command("CoderOpen", function()
|
||||
require("codetyper").setup()
|
||||
vim.cmd("CoderOpen")
|
||||
end, { desc = "Open Coder view" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderClose", function()
|
||||
require("codetyper").setup()
|
||||
vim.cmd("CoderClose")
|
||||
end, { desc = "Close Coder view" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderToggle", function()
|
||||
require("codetyper").setup()
|
||||
vim.cmd("CoderToggle")
|
||||
end, { desc = "Toggle Coder view" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderProcess", function()
|
||||
require("codetyper").setup()
|
||||
vim.cmd("CoderProcess")
|
||||
end, { desc = "Process prompt and generate code" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderTree", function()
|
||||
require("codetyper").setup()
|
||||
vim.cmd("CoderTree")
|
||||
end, { desc = "Refresh tree.log" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderTreeView", function()
|
||||
require("codetyper").setup()
|
||||
vim.cmd("CoderTreeView")
|
||||
end, { desc = "View tree.log" })
|
||||
|
||||
-- Ask panel commands
|
||||
vim.api.nvim_create_user_command("CoderAsk", function()
|
||||
require("codetyper").setup()
|
||||
vim.cmd("CoderAsk")
|
||||
end, { desc = "Open Ask panel" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderAskToggle", function()
|
||||
require("codetyper").setup()
|
||||
vim.cmd("CoderAskToggle")
|
||||
end, { desc = "Toggle Ask panel" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderAskClear", function()
|
||||
require("codetyper").setup()
|
||||
vim.cmd("CoderAskClear")
|
||||
end, { desc = "Clear Ask history" })
|
||||
Reference in New Issue
Block a user