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:
2026-01-11 15:24:06 -05:00
commit bba0647b47
29 changed files with 5503 additions and 0 deletions

88
CHANGELOG.md Normal file
View 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
View 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
View 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
View 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.
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Neovim](https://img.shields.io/badge/Neovim-0.8%2B-green.svg)](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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

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

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

View 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

View 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

View 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

View 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

View 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

View 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
View 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
View 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
View 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
View 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
View 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" })